diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..e0adafae --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Local environment and secrets +.env + +# Local root development copy +/app/ + +# Python cache +__pycache__/ +*.pyc + +# Local test/output files +output/ +approval_body.json +chat_body.json +hitl_body.json +export_test.docx +sample_grant_missing_collaborator/ \ No newline at end of file diff --git a/use-cases/Anirudh7S/research-grant-packet-assembler/.gitignore b/use-cases/Anirudh7S/research-grant-packet-assembler/.gitignore new file mode 100644 index 00000000..a5bdf536 --- /dev/null +++ b/use-cases/Anirudh7S/research-grant-packet-assembler/.gitignore @@ -0,0 +1,7 @@ +.env +__pycache__/ +*.pyc +output/ +approval_body.json +chat_body.json +hitl_body.json diff --git a/use-cases/Anirudh7S/research-grant-packet-assembler/README.md b/use-cases/Anirudh7S/research-grant-packet-assembler/README.md new file mode 100644 index 00000000..24c914a5 --- /dev/null +++ b/use-cases/Anirudh7S/research-grant-packet-assembler/README.md @@ -0,0 +1,28 @@ +# SuperDocs Builds + +Community builds: apps, integrations and extensions built on the [SuperDocs](https://superdocs.app) platform. + +SuperDocs is an AI document editor that works inside the document, not next to it. It ships as a free web app, a REST API, and an MCP server that lets AI agents read and edit documents on their own. Everything in this repository is built on that public surface. + +## What lives here + +| Folder | What belongs in it | +|---|---| +| [`use-cases/`](use-cases/) | Apps and end-to-end use cases: vertical tools, document workflows, demos that solve a real problem for a real kind of user | +| [`extensions/`](extensions/) | Extensions and developer tooling: editor integrations, plugins, CLIs, SDK wrappers, agent pipelines, anything that extends where SuperDocs can run | + +Every project is self-contained in its builder's own folder and carries its own README. + +## Contributing + +Fork the repo, build in your own folder, open a pull request. The full mechanics, including how to name your folder and what your PR description must include, are in [CONTRIBUTING.md](CONTRIBUTING.md). + +## Useful links + +- Product: [use.superdocs.app](https://use.superdocs.app) +- Developer documentation: [docs.superdocs.app](https://docs.superdocs.app) +- Contact: hello@superdocs.app + +## License + +MIT. Every contribution stays publicly credited to its author, permanently. diff --git a/use-cases/Anirudh7S/research-grant-packet-assembler/app/__init__.py b/use-cases/Anirudh7S/research-grant-packet-assembler/app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/use-cases/Anirudh7S/research-grant-packet-assembler/app/assemble.py b/use-cases/Anirudh7S/research-grant-packet-assembler/app/assemble.py new file mode 100644 index 00000000..eb852dbe --- /dev/null +++ b/use-cases/Anirudh7S/research-grant-packet-assembler/app/assemble.py @@ -0,0 +1,240 @@ +from typing import Any, Dict, List + + +def find_document( + documents: List[Dict[str, Any]], + document_type: str, +) -> Dict[str, Any]: + """Find the first document matching a document type.""" + + for document in documents: + if document["document_type"] == document_type: + return document + + return {} + + +def build_grant_package( + extracted_documents: List[Dict[str, Any]], +) -> Dict[str, Any]: + """Assemble extracted grant facts into one structured package.""" + + research = find_document( + extracted_documents, + "research_narrative", + ) + + data_management = find_document( + extracted_documents, + "data_management_plan", + ) + + facilities = find_document( + extracted_documents, + "facilities_statement", + ) + + budget = find_document( + extracted_documents, + "budget_justification", + ) + + investigator_documents = [ + document + for document in extracted_documents + if document["document_type"] == "investigator_cv" + ] + + research_facts = research.get("facts", {}) + dmp_facts = data_management.get("facts", {}) + facilities_facts = facilities.get("facts", {}) + budget_facts = budget.get("facts", {}) + + investigators = [] + + for document in investigator_documents: + facts = document.get("facts", {}) + + investigators.append( + { + "filename": document["filename"], + "name": facts.get("name", ""), + "current_position": facts.get( + "current_position", + "", + ), + "education": facts.get( + "education", + [], + ), + "research_interests": facts.get( + "research_interests", + [], + ), + "professional_experience": facts.get( + "professional_experience", + [], + ), + "selected_publications": facts.get( + "selected_publications", + [], + ), + "awards": facts.get( + "awards", + [], + ), + "current_grant_role": facts.get( + "current_grant_role", + "", + ), + } + ) + + grant_package = { + "project": { + "title": research_facts.get( + "project_title", + "", + ), + "principal_investigator": research_facts.get( + "principal_investigator", + "", + ), + "co_investigator": research_facts.get( + "co_investigator", + "", + ), + }, + + "research_narrative": { + "project_summary": research_facts.get( + "project_summary", + "", + ), + "specific_aims": research_facts.get( + "specific_aims", + [], + ), + "research_approach": research_facts.get( + "research_approach", + "", + ), + "expected_outcomes": research_facts.get( + "expected_outcomes", + "", + ), + "significance": research_facts.get( + "significance", + "", + ), + }, + + "data_management_plan": { + "project": dmp_facts.get( + "project", + "", + ), + "principal_investigator": dmp_facts.get( + "principal_investigator", + "", + ), + "co_investigator": dmp_facts.get( + "co_investigator", + "", + ), + "data_types": dmp_facts.get( + "data_types", + "", + ), + "data_collection": dmp_facts.get( + "data_collection", + "", + ), + "data_storage": dmp_facts.get( + "data_storage", + "", + ), + "data_security": dmp_facts.get( + "data_security", + "", + ), + "data_quality": dmp_facts.get( + "data_quality", + "", + ), + "data_sharing": dmp_facts.get( + "data_sharing", + "", + ), + "data_retention": dmp_facts.get( + "data_retention", + "", + ), + }, + + "investigators": investigators, + + "facilities_statement": { + "project": facilities_facts.get( + "project", + "", + ), + "facilities_and_sites": facilities_facts.get( + "facilities_and_sites", + "", + ), + "space_and_screening": facilities_facts.get( + "space_and_screening", + "", + ), + "data_systems": facilities_facts.get( + "data_systems", + "", + ), + "institutional_support": facilities_facts.get( + "institutional_support", + "", + ), + "investigator_responsibilities": facilities_facts.get( + "investigator_responsibilities", + "", + ), + }, + + "budget_justification": { + "personnel": budget_facts.get( + "personnel", + "", + ), + "principal_investigator": budget_facts.get( + "principal_investigator", + "", + ), + "co_investigator": budget_facts.get( + "co_investigator", + "", + ), + "participant_and_site_activities": budget_facts.get( + "participant_and_site_activities", + "", + ), + "data_management": budget_facts.get( + "data_management", + "", + ), + "dissemination": budget_facts.get( + "dissemination", + "", + ), + }, + + "source_documents": [ + { + "filename": document["filename"], + "document_type": document["document_type"], + } + for document in extracted_documents + ], + } + + return grant_package \ No newline at end of file diff --git a/use-cases/Anirudh7S/research-grant-packet-assembler/app/biosketch.py b/use-cases/Anirudh7S/research-grant-packet-assembler/app/biosketch.py new file mode 100644 index 00000000..1eb90a8b --- /dev/null +++ b/use-cases/Anirudh7S/research-grant-packet-assembler/app/biosketch.py @@ -0,0 +1,243 @@ +from typing import Any, Dict, List + + +BIOSKETCH_SECTIONS = [ + "Personal Information", + "Current Position", + "Education and Training", + "Research Interests", + "Professional Experience", + "Selected Publications", + "Awards and Honors", + "Current Grant Role", +] + + +def _clean_list(values: List[str]) -> List[str]: + """Return non-empty, normalized list values.""" + return [ + str(value).strip() + for value in values + if str(value).strip() + ] + + +def build_funder_biosketch( + investigator_facts: Dict[str, Any], +) -> Dict[str, Any]: + """ + Convert normalized investigator CV facts into a funder-style + biosketch without dropping extracted content. + """ + + name = str( + investigator_facts.get("name", "") + ).strip() + + current_position = str( + investigator_facts.get("current_position", "") + ).strip() + + education = _clean_list( + investigator_facts.get("education", []) + ) + + research_interests = _clean_list( + investigator_facts.get("research_interests", []) + ) + + professional_experience = _clean_list( + investigator_facts.get("professional_experience", []) + ) + + selected_publications = _clean_list( + investigator_facts.get("selected_publications", []) + ) + + awards = _clean_list( + investigator_facts.get("awards", []) + ) + + current_grant_role = str( + investigator_facts.get("current_grant_role", "") + ).strip() + + return { + "name": name, + "sections": { + "Personal Information": { + "name": name, + }, + "Current Position": { + "content": current_position, + }, + "Education and Training": { + "items": education, + }, + "Research Interests": { + "items": research_interests, + }, + "Professional Experience": { + "items": professional_experience, + }, + "Selected Publications": { + "items": selected_publications, + }, + "Awards and Honors": { + "items": awards, + }, + "Current Grant Role": { + "content": current_grant_role, + }, + }, + } + + +def render_funder_biosketch( + biosketch: Dict[str, Any], +) -> str: + """Render a structured biosketch as readable text.""" + + sections = biosketch.get("sections", {}) + + lines: List[str] = [] + + name = sections.get( + "Personal Information", + {}, + ).get("name", "") + + if name: + lines.append(name) + lines.append("") + + for section_name in BIOSKETCH_SECTIONS[1:]: + section = sections.get( + section_name, + {}, + ) + + lines.append(section_name) + + content = section.get("content", "") + + if content: + lines.append(content) + + for item in section.get("items", []): + lines.append(f"- {item}") + + lines.append("") + + return "\n".join(lines).strip() + + +def compare_source_content( + investigator_facts: Dict[str, Any], + biosketch: Dict[str, Any], +) -> Dict[str, Any]: + """ + Verify that every extracted CV fact is represented in the + generated biosketch. + + This is a preservation check, not a semantic similarity score. + """ + + source_fields = { + "name": investigator_facts.get("name", ""), + "current_position": investigator_facts.get( + "current_position", + "", + ), + "education": investigator_facts.get( + "education", + [], + ), + "research_interests": investigator_facts.get( + "research_interests", + [], + ), + "professional_experience": investigator_facts.get( + "professional_experience", + [], + ), + "selected_publications": investigator_facts.get( + "selected_publications", + [], + ), + "awards": investigator_facts.get( + "awards", + [], + ), + "current_grant_role": investigator_facts.get( + "current_grant_role", + "", + ), + } + + rendered = render_funder_biosketch( + biosketch + ).lower() + + missing: List[str] = [] + + for field, values in source_fields.items(): + if isinstance(values, list): + for value in values: + value = str(value).strip() + + if value and value.lower() not in rendered: + missing.append( + f"{field}: {value}" + ) + + else: + value = str(values).strip() + + if value and value.lower() not in rendered: + missing.append( + f"{field}: {value}" + ) + + return { + "preserved": len(missing) == 0, + "missing_content": missing, + "source_fields_checked": len( + [ + value + for value in source_fields.values() + if value + ] + ), + } + + +def convert_investigator_to_biosketch( + investigator_facts: Dict[str, Any], +) -> Dict[str, Any]: + """ + Complete CV-to-biosketch conversion with a content-preservation + check. + """ + + biosketch = build_funder_biosketch( + investigator_facts + ) + + preservation = compare_source_content( + investigator_facts, + biosketch, + ) + + return { + "name": investigator_facts.get( + "name", + "", + ), + "format": "funder_biosketch", + "biosketch": biosketch, + "rendered_text": render_funder_biosketch( + biosketch + ), + "content_preservation": preservation, + } \ No newline at end of file diff --git a/use-cases/Anirudh7S/research-grant-packet-assembler/app/classify.py b/use-cases/Anirudh7S/research-grant-packet-assembler/app/classify.py new file mode 100644 index 00000000..4193278e --- /dev/null +++ b/use-cases/Anirudh7S/research-grant-packet-assembler/app/classify.py @@ -0,0 +1,52 @@ +from pathlib import Path + + +DOCUMENT_TYPES = { + "research_narrative", + "data_management_plan", + "investigator_cv", + "facilities_statement", + "budget_justification", + "unknown", +} + + +def classify_document(filename: str, content: str = "") -> str: + """Classify a grant document using filename first, then content.""" + + name = Path(filename).stem.lower().replace("-", "_").replace(" ", "_") + text = content.lower() + + # Filename is the strongest signal. + if "budget" in name: + return "budget_justification" + + if "data_management" in name: + return "data_management_plan" + + if "research_narrative" in name: + return "research_narrative" + + if "facilities" in name: + return "facilities_statement" + + if "investigator" in name or name.endswith("_cv") or "_cv_" in name: + return "investigator_cv" + + # Content-based fallback. + if "budget justification" in text: + return "budget_justification" + + if "data management plan" in text: + return "data_management_plan" + + if "research narrative" in text: + return "research_narrative" + + if "facilities statement" in text: + return "facilities_statement" + + if "curriculum vitae" in text or "professional experience" in text: + return "investigator_cv" + + return "unknown" \ No newline at end of file diff --git a/use-cases/Anirudh7S/research-grant-packet-assembler/app/extract.py b/use-cases/Anirudh7S/research-grant-packet-assembler/app/extract.py new file mode 100644 index 00000000..0fd866ec --- /dev/null +++ b/use-cases/Anirudh7S/research-grant-packet-assembler/app/extract.py @@ -0,0 +1,538 @@ +import re +from typing import Any, Dict, List + + +def clean_text(text: str) -> str: + """Normalize whitespace without changing the actual wording.""" + return re.sub(r"\s+", " ", text).strip() + + +def extract_labeled_section( + content: str, + start_label: str, + end_labels: List[str], +) -> str: + """Extract text between one section heading and the next known heading.""" + + if end_labels: + escaped_end_labels = "|".join( + re.escape(label) for label in end_labels + ) + + pattern = ( + re.escape(start_label) + + r"\s*(.*?)(?=\n(?:" + + escaped_end_labels + + r")\s*\n|\Z)" + ) + else: + pattern = re.escape(start_label) + r"\s*(.*)\Z" + + match = re.search( + pattern, + content, + flags=re.IGNORECASE | re.DOTALL, + ) + + if not match: + return "" + + return clean_text(match.group(1)) + + +def extract_labeled_lines( + content: str, + start_label: str, + end_labels: List[str], +) -> List[str]: + """Extract a section while preserving individual lines.""" + + lines = content.splitlines() + collecting = False + result = [] + + normalized_end_labels = { + label.strip().lower() + for label in end_labels + } + + for line in lines: + stripped = line.strip() + + if stripped.lower() == start_label.strip().lower(): + collecting = True + continue + + if collecting and stripped.lower() in normalized_end_labels: + break + + if collecting and stripped: + result.append(clean_text(stripped)) + + return result + + +def extract_labeled_lines_aliases( + content: str, + start_labels: List[str], + end_labels: List[str], +) -> List[str]: + """Extract lines from a section supporting multiple heading aliases.""" + + lines = content.splitlines() + collecting = False + result = [] + + normalized_starts = { + label.strip().lower() + for label in start_labels + } + + normalized_ends = { + label.strip().lower() + for label in end_labels + } + + for line in lines: + stripped = line.strip() + normalized = stripped.lower() + + if normalized in normalized_starts: + collecting = True + continue + + if collecting and normalized in normalized_ends: + break + + if collecting and stripped: + result.append(clean_text(stripped)) + + return result + + +def extract_research_narrative(content: str) -> Dict[str, Any]: + """Extract structured facts from a research narrative.""" + + facts: Dict[str, Any] = { + "project_title": "", + "principal_investigator": "", + "co_investigator": "", + "project_summary": "", + "specific_aims": [], + "research_approach": "", + "expected_outcomes": "", + "significance": "", + } + + # Project title + match = re.search( + r"Project Title:\s*(.+)", + content, + flags=re.IGNORECASE, + ) + + if match: + facts["project_title"] = clean_text(match.group(1)) + + # Principal Investigator + match = re.search( + r"Principal Investigator:\s*\n\s*(.+)", + content, + flags=re.IGNORECASE, + ) + + if match: + facts["principal_investigator"] = clean_text(match.group(1)) + + # Co-Investigator + match = re.search( + r"Co-Investigator:\s*\n\s*(.+)", + content, + flags=re.IGNORECASE, + ) + + if match: + facts["co_investigator"] = clean_text(match.group(1)) + + section_labels = [ + "Project Summary", + "Specific Aim 1", + "Specific Aim 2", + "Specific Aim 3", + "Research Approach", + "Expected Outcomes", + "Significance", + "Investigators", + ] + + facts["project_summary"] = extract_labeled_section( + content, + "Project Summary", + section_labels[1:], + ) + + aim_1 = extract_labeled_section( + content, + "Specific Aim 1", + section_labels[2:], + ) + + aim_2 = extract_labeled_section( + content, + "Specific Aim 2", + section_labels[3:], + ) + + aim_3 = extract_labeled_section( + content, + "Specific Aim 3", + section_labels[4:], + ) + + facts["specific_aims"] = [ + aim + for aim in [aim_1, aim_2, aim_3] + if aim + ] + + facts["research_approach"] = extract_labeled_section( + content, + "Research Approach", + section_labels[5:], + ) + + facts["expected_outcomes"] = extract_labeled_section( + content, + "Expected Outcomes", + section_labels[6:], + ) + + facts["significance"] = extract_labeled_section( + content, + "Significance", + section_labels[7:], + ) + + return facts + + +def extract_data_management_plan(content: str) -> Dict[str, Any]: + """Extract structured facts from a data management plan.""" + + facts: Dict[str, Any] = { + "project": "", + "principal_investigator": "", + "co_investigator": "", + "data_types": "", + "data_collection": "", + "data_storage": "", + "data_security": "", + "data_quality": "", + "data_sharing": "", + "data_retention": "", + } + + match = re.search( + r"Project:\s*(.+)", + content, + flags=re.IGNORECASE, + ) + + if match: + facts["project"] = clean_text(match.group(1)) + + match = re.search( + r"Principal Investigator:\s*(.+)", + content, + flags=re.IGNORECASE, + ) + + if match: + facts["principal_investigator"] = clean_text(match.group(1)) + + match = re.search( + r"Co-Investigator:\s*(.+)", + content, + flags=re.IGNORECASE, + ) + + if match: + facts["co_investigator"] = clean_text(match.group(1)) + + section_labels = [ + "Data Types", + "Data Collection", + "Data Storage", + "Data Security", + "Data Quality", + "Data Sharing", + "Data Retention", + ] + + for index, section in enumerate(section_labels): + end_labels = section_labels[index + 1:] + + facts_key = section.lower().replace(" ", "_") + + facts[facts_key] = extract_labeled_section( + content, + section, + end_labels, + ) + + return facts + + +def extract_investigator_cv(content: str) -> Dict[str, Any]: + """Extract structured facts from different investigator CV formats.""" + + facts: Dict[str, Any] = { + "name": "", + "current_position": "", + "education": [], + "research_interests": [], + "professional_experience": [], + "selected_publications": [], + "awards": [], + "current_grant_role": "", + } + + lines = [ + line.strip() + for line in content.splitlines() + if line.strip() + ] + + # The investigator name is normally the second non-empty line. + if len(lines) >= 2: + facts["name"] = lines[1] + + # Supported heading aliases + current_position_labels = [ + "Current Position", + "Professional Role", + ] + + education_labels = [ + "Education", + "Academic Training", + ] + + research_labels = [ + "Research Interests", + "Research Focus", + ] + + experience_labels = [ + "Professional Experience", + "Employment History", + ] + + publication_labels = [ + "Selected Publications", + "Selected Research", + ] + + award_labels = [ + "Awards", + "Honors", + ] + + grant_role_labels = [ + "Current Grant Role", + "Grant Responsibility", + ] + + all_section_labels = ( + current_position_labels + + education_labels + + research_labels + + experience_labels + + publication_labels + + award_labels + + grant_role_labels + ) + + # Current position / professional role + facts["current_position"] = "" + + for label in current_position_labels: + facts["current_position"] = extract_labeled_section( + content, + label, + [x for x in all_section_labels if x not in current_position_labels], + ) + + if facts["current_position"]: + break + + # Education / academic training + facts["education"] = extract_labeled_lines_aliases( + content, + education_labels, + [ + x + for x in all_section_labels + if x not in education_labels + ], + ) + + # Research interests / research focus + facts["research_interests"] = extract_labeled_lines_aliases( + content, + research_labels, + [ + x + for x in all_section_labels + if x not in research_labels + ], + ) + + # Professional experience / employment history + facts["professional_experience"] = extract_labeled_lines_aliases( + content, + experience_labels, + [ + x + for x in all_section_labels + if x not in experience_labels + ], + ) + + # Selected publications / selected research + facts["selected_publications"] = extract_labeled_lines_aliases( + content, + publication_labels, + [ + x + for x in all_section_labels + if x not in publication_labels + ], + ) + + # Awards / honors + facts["awards"] = extract_labeled_lines_aliases( + content, + award_labels, + [ + x + for x in all_section_labels + if x not in award_labels + ], + ) + + # Current grant role / grant responsibility + facts["current_grant_role"] = "" + + for label in grant_role_labels: + facts["current_grant_role"] = extract_labeled_section( + content, + label, + [], + ) + + if facts["current_grant_role"]: + break + + return facts + +def extract_facilities_statement(content: str) -> Dict[str, Any]: + """Extract structured facts from a facilities statement.""" + + facts: Dict[str, Any] = { + "project": "", + "facilities_and_sites": "", + "space_and_screening": "", + "data_systems": "", + "institutional_support": "", + "investigator_responsibilities": "", + } + + # Project + match = re.search( + r"Project:\s*(.+)", + content, + flags=re.IGNORECASE, + ) + + if match: + facts["project"] = clean_text(match.group(1)) + + # Split the document into paragraphs. + paragraphs = [ + clean_text(paragraph) + for paragraph in re.split(r"\n\s*\n", content) + if clean_text(paragraph) + ] + + # Expected structure: + # 0 = FACILITIES STATEMENT + # 1 = Project: ... + # 2 = Facilities/sites paragraph + # 3 = Space/screening + data systems paragraph + # 4 = Institutional support paragraph + # 5 = Investigator responsibilities paragraph + + if len(paragraphs) >= 3: + facts["facilities_and_sites"] = paragraphs[2] + + if len(paragraphs) >= 4: + facts["space_and_screening"] = paragraphs[3] + + if len(paragraphs) >= 5: + facts["institutional_support"] = paragraphs[4] + + if len(paragraphs) >= 6: + facts["investigator_responsibilities"] = paragraphs[5] + + # Extract the data-system statement from the + # space/screening paragraph. + if facts["space_and_screening"]: + data_match = re.search( + r"The research team will use secure project systems for data collection and storage\.", + facts["space_and_screening"], + flags=re.IGNORECASE, + ) + + if data_match: + facts["data_systems"] = clean_text( + data_match.group(0) + ) + + return facts + +def extract_budget_justification(content: str) -> Dict[str, Any]: + """Extract structured facts from a budget justification.""" + + facts: Dict[str, Any] = { + "personnel": "", + "principal_investigator": "", + "co_investigator": "", + "participant_and_site_activities": "", + "data_management": "", + "dissemination": "", + } + + section_map = { + "Personnel": "personnel", + "Principal Investigator": "principal_investigator", + "Co-Investigator": "co_investigator", + "Participant and Site Activities": "participant_and_site_activities", + "Data Management": "data_management", + "Dissemination": "dissemination", + } + + section_labels = list(section_map.keys()) + + for index, section in enumerate(section_labels): + end_labels = section_labels[index + 1:] + + facts_key = section_map[section] + + facts[facts_key] = extract_labeled_section( + content, + section, + end_labels, + ) + + return facts \ No newline at end of file diff --git a/use-cases/Anirudh7S/research-grant-packet-assembler/app/ingest.py b/use-cases/Anirudh7S/research-grant-packet-assembler/app/ingest.py new file mode 100644 index 00000000..a24effb5 --- /dev/null +++ b/use-cases/Anirudh7S/research-grant-packet-assembler/app/ingest.py @@ -0,0 +1,85 @@ +from pathlib import Path +from typing import Dict, List + + +SUPPORTED_EXTENSIONS = { + ".txt", + ".md", + ".pdf", + ".docx", +} + + +def read_document(path: Path) -> str: + """Read a supported document into plain text.""" + + suffix = path.suffix.lower() + + if suffix not in SUPPORTED_EXTENSIONS: + raise ValueError( + f"Unsupported file type: {suffix}. " + f"Supported types: {sorted(SUPPORTED_EXTENSIONS)}" + ) + + if suffix in {".txt", ".md"}: + return path.read_text( + encoding="utf-8", + errors="replace", + ) + + if suffix == ".pdf": + from pypdf import PdfReader + + reader = PdfReader(str(path)) + pages = [] + + for page_number, page in enumerate(reader.pages, start=1): + text = page.extract_text() or "" + pages.append(f"[PAGE {page_number}]\n{text}") + + return "\n".join(pages) + + if suffix == ".docx": + from docx import Document + + document = Document(str(path)) + return "\n".join( + paragraph.text + for paragraph in document.paragraphs + ) + + raise ValueError(f"Unsupported file type: {suffix}") + + +def ingest_directory(directory: str | Path) -> List[Dict]: + """Read all supported documents from a directory.""" + + directory = Path(directory) + + if not directory.exists(): + raise FileNotFoundError( + f"Grant directory does not exist: {directory}" + ) + + documents = [] + + for path in sorted(directory.iterdir()): + if not path.is_file(): + continue + + if path.suffix.lower() not in SUPPORTED_EXTENSIONS: + continue + + content = read_document(path) + + documents.append( + { + "filename": path.name, + "path": str(path), + "extension": path.suffix.lower(), + "content": content, + "characters": len(content), + } + ) + + return documents \ No newline at end of file diff --git a/use-cases/Anirudh7S/research-grant-packet-assembler/app/output.py b/use-cases/Anirudh7S/research-grant-packet-assembler/app/output.py new file mode 100644 index 00000000..eeafff5e --- /dev/null +++ b/use-cases/Anirudh7S/research-grant-packet-assembler/app/output.py @@ -0,0 +1,28 @@ +import json +from pathlib import Path +from typing import Any, Dict + + +def save_grant_package( + grant_package: Dict[str, Any], + output_path: str | Path, +) -> Path: + """Save the assembled grant package as formatted JSON.""" + + output_path = Path(output_path) + + output_path.parent.mkdir( + parents=True, + exist_ok=True, + ) + + output_path.write_text( + json.dumps( + grant_package, + indent=2, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + return output_path \ No newline at end of file diff --git a/use-cases/Anirudh7S/research-grant-packet-assembler/app/page_limits.py b/use-cases/Anirudh7S/research-grant-packet-assembler/app/page_limits.py new file mode 100644 index 00000000..ff24867b --- /dev/null +++ b/use-cases/Anirudh7S/research-grant-packet-assembler/app/page_limits.py @@ -0,0 +1,133 @@ +from typing import Any, Dict + + +DEFAULT_PAGE_LIMITS = { + "research_narrative": 5, + "data_management_plan": 2, + "facilities_statement": 2, + "budget_justification": 2, + "investigator_cv": 3, +} + + +def estimate_pages( + text: str, + chars_per_page: int = 3000, +) -> int: + """ + Estimate page count from text length. + + This is intentionally conservative and deterministic for the + local validation/demo pipeline. Actual exported pagination can + vary by font, margins, spacing, and template. + """ + + normalized = " ".join( + str(text).split() + ).strip() + + if not normalized: + return 0 + + return max( + 1, + (len(normalized) + chars_per_page - 1) + // chars_per_page, + ) + + +def check_page_limit( + document_type: str, + text: str, + page_limit: int | None = None, +) -> Dict[str, Any]: + """Check whether a document fits its configured page limit.""" + + limit = page_limit + + if limit is None: + limit = DEFAULT_PAGE_LIMITS.get( + document_type + ) + + if limit is None: + return { + "checked": False, + "document_type": document_type, + "page_limit": None, + "estimated_pages": None, + "within_limit": True, + "status": "not_configured", + } + + estimated_pages = estimate_pages(text) + + return { + "checked": True, + "document_type": document_type, + "page_limit": limit, + "estimated_pages": estimated_pages, + "within_limit": estimated_pages <= limit, + "status": ( + "met" + if estimated_pages <= limit + else "exceeded" + ), + } + + +def build_tightening_instruction( + document_type: str, + page_limit: int, + estimated_pages: int, +) -> str: + """ + Build the SuperDocs instruction used when a document exceeds + its page limit. + """ + + return ( + f"Tighten the {document_type.replace('_', ' ')} " + f"so that it fits within a maximum of {page_limit} pages. " + f"The current estimated length is {estimated_pages} pages. " + "Reduce unnecessary repetition, wordiness, and redundant " + "phrasing while preserving the argument, factual claims, " + "specific aims, methodology, named investigators, numbers, " + "citations, expected outcomes, and significance. " + "Do not invent facts. Do not remove substantive evidence. " + "Do not truncate the document mechanically. " + "Rewrite sentences and paragraphs for concision instead." + ) + + +def enforce_page_limit_locally( + document_type: str, + text: str, + page_limit: int, +) -> Dict[str, Any]: + """ + Perform deterministic page-limit validation without deleting + content. + + Actual prose tightening is delegated to SuperDocs. + """ + + check = check_page_limit( + document_type, + text, + page_limit, + ) + + return { + **check, + "requires_superdocs_edit": not check["within_limit"], + "instruction": ( + build_tightening_instruction( + document_type, + page_limit, + check["estimated_pages"], + ) + if not check["within_limit"] + else None + ), + } \ No newline at end of file diff --git a/use-cases/Anirudh7S/research-grant-packet-assembler/app/pipeline.py b/use-cases/Anirudh7S/research-grant-packet-assembler/app/pipeline.py new file mode 100644 index 00000000..e5f90272 --- /dev/null +++ b/use-cases/Anirudh7S/research-grant-packet-assembler/app/pipeline.py @@ -0,0 +1,167 @@ +from pathlib import Path +from typing import Any, Dict, List + +from app.assemble import build_grant_package +from app.biosketch import convert_investigator_to_biosketch +from app.classify import classify_document +from app.extract import ( + extract_budget_justification, + extract_data_management_plan, + extract_facilities_statement, + extract_investigator_cv, + extract_research_narrative, +) +from app.ingest import ingest_directory +from app.output import save_grant_package +from app.validate import validate_documents + + +def extract_facts( + document_type: str, + content: str, +) -> Dict[str, Any]: + """Run the appropriate extractor for a document type.""" + + extractors = { + "research_narrative": extract_research_narrative, + "data_management_plan": extract_data_management_plan, + "investigator_cv": extract_investigator_cv, + "facilities_statement": extract_facilities_statement, + "budget_justification": extract_budget_justification, + } + + extractor = extractors.get(document_type) + + if extractor is None: + return {} + + return extractor(content) + + +def build_extracted_documents( + grant_directory: str | Path, +) -> List[Dict[str, Any]]: + """Ingest, classify, and extract all supported grant documents.""" + + documents = ingest_directory(grant_directory) + + extracted_documents = [] + + for document in documents: + document_type = classify_document( + document["filename"], + document["content"], + ) + + facts = extract_facts( + document_type, + document["content"], + ) + + extracted_documents.append( + { + "filename": document["filename"], + "path": document["path"], + "extension": document["extension"], + "document_type": document_type, + "characters": document["characters"], + "facts": facts, + } + ) + + return extracted_documents + + +def build_biosketches( + extracted_documents: List[Dict[str, Any]], +) -> List[Dict[str, Any]]: + """ + Convert every investigator CV into a funder-style biosketch. + + The original extracted CV facts remain untouched. Each biosketch + carries an explicit content-preservation result. + """ + + biosketches = [] + + for document in extracted_documents: + if document["document_type"] != "investigator_cv": + continue + + facts = document.get("facts", {}) + + biosketch = convert_investigator_to_biosketch( + facts + ) + + biosketches.append( + { + "source_document": document["filename"], + "investigator": biosketch["name"], + "format": biosketch["format"], + "rendered_text": biosketch["rendered_text"], + "content_preservation": biosketch[ + "content_preservation" + ], + } + ) + + return biosketches + + +def run_validation( + grant_directory: str | Path, +) -> Dict[str, Any]: + """Build the structured grant corpus and validate it.""" + + extracted_documents = build_extracted_documents( + grant_directory + ) + + validation = validate_documents( + extracted_documents + ) + + return { + "documents": extracted_documents, + "validation": validation, + } + + +def run_grant_assembly( + grant_directory: str | Path, + output_path: str | Path = "output/grant_package.json", +) -> Dict[str, Any]: + """Run extraction, biosketch conversion, validation, assembly, and persistence.""" + + extracted_documents = build_extracted_documents( + grant_directory + ) + + biosketches = build_biosketches( + extracted_documents + ) + + validation = validate_documents( + extracted_documents + ) + + grant_package = build_grant_package( + extracted_documents + ) + + # Keep generated biosketches alongside the assembled grant package. + grant_package["biosketches"] = biosketches + + saved_path = save_grant_package( + grant_package, + output_path, + ) + + return { + "documents": extracted_documents, + "biosketches": biosketches, + "validation": validation, + "grant_package": grant_package, + "output_path": str(saved_path), + } \ No newline at end of file diff --git a/use-cases/Anirudh7S/research-grant-packet-assembler/app/superdocs.py b/use-cases/Anirudh7S/research-grant-packet-assembler/app/superdocs.py new file mode 100644 index 00000000..cff3c14b --- /dev/null +++ b/use-cases/Anirudh7S/research-grant-packet-assembler/app/superdocs.py @@ -0,0 +1,206 @@ +import os +from typing import Any, Dict + +import requests +from dotenv import load_dotenv + + +# Load variables from the project's .env file. +load_dotenv() + + +BASE_URL = os.getenv( + "SUPERDOCS_API_URL", + "https://api.superdocs.app", +).rstrip("/") + "/v1" + + +class SuperDocsError(Exception): + """Raised when a SuperDocs API request fails.""" + + +class SuperDocsClient: + """Small Python client for the SuperDocs API.""" + + def __init__(self, api_key: str | None = None): + self.api_key = api_key or os.getenv("SUPERDOCS_API_KEY") + + if not self.api_key: + raise SuperDocsError( + "SUPERDOCS_API_KEY environment variable is not set." + ) + + self.headers = { + "Authorization": f"Bearer {self.api_key}", + } + + def _request( + self, + method: str, + endpoint: str, + **kwargs: Any, + ) -> Dict[str, Any]: + """Make an API request and return JSON.""" + + url = f"{BASE_URL}{endpoint}" + + response = requests.request( + method, + url, + headers=self.headers, + timeout=60, + **kwargs, + ) + + if not response.ok: + raise SuperDocsError( + f"SuperDocs API error " + f"{response.status_code}: {response.text}" + ) + + try: + return response.json() + except ValueError as exc: + raise SuperDocsError( + f"SuperDocs returned invalid JSON: {response.text}" + ) from exc + + def init_session(self) -> Dict[str, Any]: + """Create a new SuperDocs session.""" + + return self._request( + "POST", + "/sessions/init", + json={}, + ) + + def upload_document( + self, + file_path: str, + session_id: str, + ) -> Dict[str, Any]: + """Upload a document into a SuperDocs session.""" + + path = os.path.abspath(file_path) + + if not os.path.isfile(path): + raise SuperDocsError( + f"Document not found: {path}" + ) + + with open(path, "rb") as file: + files = { + "file": ( + os.path.basename(path), + file, + ) + } + + data = { + "session_id": session_id, + } + + return self._request( + "POST", + "/documents/upload", + files=files, + data=data, + ) + + def chat( + self, + session_id: str, + message: str, + ) -> Dict[str, Any]: + """Send a synchronous chat request.""" + + return self._request( + "POST", + "/chat", + json={ + "session_id": session_id, + "message": message, + }, + ) + + def chat_async( + self, + session_id: str, + message: str, + approval_mode: str | None = None, + ) -> Dict[str, Any]: + """Queue an asynchronous chat request.""" + + body: Dict[str, Any] = { + "session_id": session_id, + "message": message, + } + + if approval_mode is not None: + body["approval_mode"] = approval_mode + + return self._request( + "POST", + "/chat/async", + json=body, + ) + + def get_job( + self, + job_id: str, + ) -> Dict[str, Any]: + """Get the current status of an asynchronous job.""" + + return self._request( + "GET", + f"/jobs/{job_id}", + ) + + def approve_change( + self, + session_id: str, + job_id: str, + change_id: str, + approved: bool = True, + ) -> Dict[str, Any]: + """Approve or reject a pending HITL change.""" + + return self._request( + "POST", + f"/chat/{session_id}/approve", + json={ + "approved": approved, + "job_id": job_id, + "change_id": change_id, + }, + ) + + def export_document( + self, + session_id: str, + html: str, + filename: str = "grant_packet.docx", + ) -> bytes: + """Export the current SuperDocs document as a DOCX file.""" + + url = f"{BASE_URL}/documents/export" + + response = requests.post( + url, + headers=self.headers, + json={ + "session_id": session_id, + "html": html, + "format": "docx", + "filename": filename, + }, + timeout=60, + ) + + if not response.ok: + raise SuperDocsError( + f"SuperDocs export error " + f"{response.status_code}: {response.text}" + ) + + return response.content \ No newline at end of file diff --git a/use-cases/Anirudh7S/research-grant-packet-assembler/app/validate.py b/use-cases/Anirudh7S/research-grant-packet-assembler/app/validate.py new file mode 100644 index 00000000..f9cfcfbf --- /dev/null +++ b/use-cases/Anirudh7S/research-grant-packet-assembler/app/validate.py @@ -0,0 +1,489 @@ +from typing import Any, Dict, List +import re + + +def normalize_value(value: Any) -> str: + """Normalize a value for comparison.""" + + if value is None: + return "" + + return " ".join(str(value).lower().split()) + + +def normalize_person_name(value: Any) -> str: + """Extract and normalize a person's name from a name or sentence.""" + + text = normalize_value(value) + + if not text: + return "" + + # Known investigator names in the current grant corpus. + # This prevents descriptive sentences from being treated + # as the person's full name. + known_names = [ + "dr. ananya sharma", + "dr. rahul verma", + ] + + for name in known_names: + if name in text: + return name + + # Generic fallback for future investigator names. + match = re.search( + r"\bdr\.?\s+[a-z]+(?:\s+[a-z]+)+", + text, + flags=re.IGNORECASE, + ) + + if match: + candidate = match.group(0) + + stop_words = { + "will", + "is", + "was", + "are", + "were", + "serves", + "serve", + "provides", + "provide", + "providing", + "coordinates", + "coordinate", + "leads", + "lead", + "oversees", + "oversee", + } + + words = candidate.split() + cleaned_words = [] + + for word in words: + if word.lower() in stop_words: + break + + cleaned_words.append(word) + + return normalize_value( + " ".join(cleaned_words) + ) + + return text + + +def add_issue( + issues: List[Dict[str, Any]], + severity: str, + issue_type: str, + message: str, + documents: List[str], +) -> None: + """Add a structured validation issue.""" + + issues.append( + { + "severity": severity, + "type": issue_type, + "message": message, + "documents": documents, + } + ) + + +def validate_project_identity( + extracted_documents: List[Dict[str, Any]], +) -> List[Dict[str, Any]]: + """Validate project title/name consistency across documents.""" + + issues: List[Dict[str, Any]] = [] + + project_values: Dict[str, List[str]] = {} + + for document in extracted_documents: + filename = document["filename"] + facts = document["facts"] + + project = ( + facts.get("project_title") + or facts.get("project") + or "" + ) + + project = str(project).strip() + + if not project: + continue + + normalized = normalize_value(project) + + project_values.setdefault( + normalized, + [], + ).append(filename) + + if len(project_values) > 1: + details = [] + + for value, filenames in project_values.items(): + details.append( + f"{value} ({', '.join(filenames)})" + ) + + add_issue( + issues, + "error", + "project_mismatch", + "Project identity differs across documents: " + + "; ".join(details), + [ + filename + for filenames in project_values.values() + for filename in filenames + ], + ) + + return issues + + +def validate_investigators( + extracted_documents: List[Dict[str, Any]], +) -> List[Dict[str, Any]]: + """Validate PI and Co-Investigator identity consistency.""" + + issues: List[Dict[str, Any]] = [] + + pi_values: Dict[str, List[str]] = {} + co_pi_values: Dict[str, List[str]] = {} + + for document in extracted_documents: + filename = document["filename"] + facts = document["facts"] + + pi = normalize_person_name( + facts.get("principal_investigator", "") + ) + + co_pi = normalize_person_name( + facts.get("co_investigator", "") + ) + + if pi: + pi_values.setdefault( + pi, + [], + ).append(filename) + + if co_pi: + co_pi_values.setdefault( + co_pi, + [], + ).append(filename) + + if len(pi_values) > 1: + details = [] + + for value, filenames in pi_values.items(): + details.append( + f"{value} ({', '.join(filenames)})" + ) + + add_issue( + issues, + "error", + "principal_investigator_mismatch", + "Principal Investigator differs across documents: " + + "; ".join(details), + [ + filename + for filenames in pi_values.values() + for filename in filenames + ], + ) + + if len(co_pi_values) > 1: + details = [] + + for value, filenames in co_pi_values.items(): + details.append( + f"{value} ({', '.join(filenames)})" + ) + + add_issue( + issues, + "error", + "co_investigator_mismatch", + "Co-Investigator differs across documents: " + + "; ".join(details), + [ + filename + for filenames in co_pi_values.values() + for filename in filenames + ], + ) + + return issues + + +def validate_required_fields( + extracted_documents: List[Dict[str, Any]], +) -> List[Dict[str, Any]]: + """Detect important missing fields in extracted documents.""" + + issues: List[Dict[str, Any]] = [] + + required_fields = { + "research_narrative": [ + "project_title", + "principal_investigator", + "co_investigator", + "project_summary", + "specific_aims", + "research_approach", + "expected_outcomes", + "significance", + ], + "data_management_plan": [ + "project", + "principal_investigator", + "co_investigator", + "data_types", + "data_collection", + "data_storage", + "data_security", + "data_quality", + "data_sharing", + "data_retention", + ], + "investigator_cv": [ + "name", + "current_position", + "education", + "research_interests", + "professional_experience", + "selected_publications", + "awards", + "current_grant_role", + ], + "facilities_statement": [ + "project", + "facilities_and_sites", + "space_and_screening", + "data_systems", + "institutional_support", + "investigator_responsibilities", + ], + "budget_justification": [ + "personnel", + "principal_investigator", + "co_investigator", + "participant_and_site_activities", + "data_management", + "dissemination", + ], + } + + for document in extracted_documents: + filename = document["filename"] + document_type = document["document_type"] + facts = document["facts"] + + fields = required_fields.get( + document_type, + [], + ) + + for field in fields: + value = facts.get(field) + + missing = ( + value is None + or value == "" + or value == [] + ) + + if missing: + add_issue( + issues, + "warning", + "missing_field", + f"Missing required field '{field}' " + f"in {filename}.", + [filename], + ) + + return issues + + +def validate_investigator_roles( + extracted_documents: List[Dict[str, Any]], +) -> List[Dict[str, Any]]: + """Check that investigator CVs contain expected grant roles.""" + + issues: List[Dict[str, Any]] = [] + + for document in extracted_documents: + if document["document_type"] != "investigator_cv": + continue + + filename = document["filename"] + facts = document["facts"] + + name = normalize_person_name( + facts.get("name", "") + ) + + role = normalize_value( + facts.get("current_grant_role", "") + ) + + if not role: + continue + + if "ananya sharma" in name: + if "principal investigator" not in role: + add_issue( + issues, + "error", + "investigator_role_mismatch", + f"{filename} does not identify " + "Dr. Ananya Sharma as Principal Investigator.", + [filename], + ) + + if "rahul verma" in name: + if "co-investigator" not in role: + add_issue( + issues, + "error", + "investigator_role_mismatch", + f"{filename} does not identify " + "Dr. Rahul Verma as Co-Investigator.", + [filename], + ) + + return issues + + +def validate_collaborator_documents( + extracted_documents: List[Dict[str, Any]], +) -> List[Dict[str, Any]]: + """ + Check that every named investigator has a corresponding + investigator document in the packet. + """ + + issues: List[Dict[str, Any]] = [] + + # Names for which an investigator CV/biosketch exists. + documented_people = set() + + for document in extracted_documents: + if document["document_type"] != "investigator_cv": + continue + + name = normalize_person_name( + document["facts"].get("name", "") + ) + + if name: + documented_people.add(name) + + # Names mentioned by the grant documents. + named_people = set() + + for document in extracted_documents: + facts = document["facts"] + + for field in ( + "principal_investigator", + "co_investigator", + ): + name = normalize_person_name( + facts.get(field, "") + ) + + if name: + named_people.add(name) + + # Every named investigator must have a corresponding + # investigator document. + for person in sorted(named_people): + if person not in documented_people: + add_issue( + issues, + "error", + "missing_collaborator_document", + f"No investigator document found for {person}.", + [], + ) + + return issues + + +def validate_documents( + extracted_documents: List[Dict[str, Any]], +) -> Dict[str, Any]: + """Run all cross-document validation checks.""" + + issues: List[Dict[str, Any]] = [] + + # 1. Project identity consistency + issues.extend( + validate_project_identity( + extracted_documents + ) + ) + + # 2. Investigator identity consistency + issues.extend( + validate_investigators( + extracted_documents + ) + ) + + # 3. Required fields + issues.extend( + validate_required_fields( + extracted_documents + ) + ) + + # 4. Investigator role consistency + issues.extend( + validate_investigator_roles( + extracted_documents + ) + ) + + # 5. Every named investigator must have + # a corresponding investigator document. + issues.extend( + validate_collaborator_documents( + extracted_documents + ) + ) + + errors = [ + issue + for issue in issues + if issue["severity"] == "error" + ] + + warnings = [ + issue + for issue in issues + if issue["severity"] == "warning" + ] + + return { + "valid": len(errors) == 0, + "errors": errors, + "warnings": warnings, + "issues": issues, + "issue_count": len(issues), + } \ No newline at end of file diff --git a/use-cases/Anirudh7S/research-grant-packet-assembler/app/workflow.py b/use-cases/Anirudh7S/research-grant-packet-assembler/app/workflow.py new file mode 100644 index 00000000..3d50212c --- /dev/null +++ b/use-cases/Anirudh7S/research-grant-packet-assembler/app/workflow.py @@ -0,0 +1,435 @@ +from pathlib import Path +from typing import Any, Dict +import re +import time + +from app.page_limits import enforce_page_limit_locally +from app.pipeline import run_grant_assembly +from app.superdocs import SuperDocsClient + + +def _approve_pending_changes( + client: SuperDocsClient, + session_id: str, + job: Dict[str, Any], +) -> Dict[str, Any]: + """ + Approve all pending HITL changes returned by a SuperDocs job. + """ + + metadata = job.get("metadata", {}) + pending_changes = metadata.get( + "pending_changes", + [], + ) + + approvals = [] + + for change in pending_changes: + approval = client.approve_change( + session_id=session_id, + job_id=job["job_id"], + change_id=change["change_id"], + approved=True, + ) + + approvals.append(approval) + + return { + "approved_count": len(approvals), + "approvals": approvals, + } + + +def wait_for_job( + client: SuperDocsClient, + job_id: str, + max_attempts: int = 30, + poll_seconds: int = 5, +) -> Dict[str, Any]: + """ + Poll an asynchronous SuperDocs job until it reaches a terminal state. + + The bounded polling loop prevents an integration from waiting forever. + """ + + for _ in range(max_attempts): + job = client.get_job(job_id) + + status = job.get("status") + + if status in { + "completed", + "failed", + "cancelled", + "awaiting_approval", + }: + return job + + time.sleep(poll_seconds) + + raise RuntimeError( + f"SuperDocs job {job_id} did not finish within " + f"{max_attempts * poll_seconds} seconds." + ) + + +def _html_to_text(html: str) -> str: + """ + Convert SuperDocs HTML into plain text for local page estimation. + """ + + text = re.sub( + r"<[^>]+>", + " ", + html, + ) + + text = ( + text + .replace(" ", " ") + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .strip() + ) + + return text + + +def _export_superdocs_document( + client: SuperDocsClient, + session_id: str, + updated_html: str, + document_path: Path, +) -> Dict[str, Any]: + """ + Export the final SuperDocs HTML as a DOCX file. + """ + + export_path = ( + document_path.parent + / f"{document_path.stem}_superdocs_export.docx" + ) + + exported_bytes = client.export_document( + session_id=session_id, + html=updated_html, + filename=export_path.name, + ) + + export_path.write_bytes( + exported_bytes + ) + + return { + "exported": True, + "export_path": str(export_path), + "export_filename": export_path.name, + "export_size_bytes": len(exported_bytes), + } + + +def run_page_limit_workflow( + client: SuperDocsClient, + document_path: Path, + document_type: str, + page_limit: int, +) -> Dict[str, Any]: + """ + Upload a document to SuperDocs and request prose tightening when + the local page-limit check says editing is required. + + The workflow performs: + + 1. Local page-limit check. + 2. SuperDocs upload. + 3. Async AI editing. + 4. HITL approval, including multiple approval batches. + 5. Final document retrieval. + 6. Local page-limit recheck. + 7. DOCX export. + """ + + original_text = document_path.read_text( + encoding="utf-8" + ) + + before_check = enforce_page_limit_locally( + document_type, + original_text, + page_limit, + ) + + result: Dict[str, Any] = { + "document": str(document_path), + "document_type": document_type, + "page_limit": page_limit, + "before": before_check, + "edited": False, + "approval": None, + "job": None, + "job_after_approval": None, + "updated_html": None, + "after": None, + "export": None, + } + + # --------------------------------------------------------- + # If the document already fits, there is no reason to call + # SuperDocs. + # --------------------------------------------------------- + + if not before_check["requires_superdocs_edit"]: + result["after"] = before_check + return result + + # --------------------------------------------------------- + # 1. Create SuperDocs session + # --------------------------------------------------------- + + session = client.init_session() + + session_id = session["session_id"] + + result["session_id"] = session_id + + # --------------------------------------------------------- + # 2. Upload document + # --------------------------------------------------------- + + upload = client.upload_document( + str(document_path), + session_id, + ) + + result["document_id"] = upload.get( + "document_id" + ) + + result["upload"] = upload + + # --------------------------------------------------------- + # 3. Request async page-limit editing + # --------------------------------------------------------- + + job = client.chat_async( + session_id, + before_check["instruction"], + approval_mode="ask_every_time", + ) + + job_id = job["job_id"] + + result["job_id"] = job_id + + # --------------------------------------------------------- + # 4. Wait for initial AI processing / HITL state + # --------------------------------------------------------- + + job_result = wait_for_job( + client, + job_id, + ) + + result["job"] = job_result + + # --------------------------------------------------------- + # 5. Approve ALL HITL batches until SuperDocs completes + # --------------------------------------------------------- + + total_approved = 0 + approval_batches = [] + + while job_result.get("status") == "awaiting_approval": + + approval = _approve_pending_changes( + client, + session_id, + job_result, + ) + + approved_count = approval.get( + "approved_count", + 0, + ) + + total_approved += approved_count + + approval_batches.append( + approval + ) + + # Safety guard: + # If SuperDocs asks for approval but returns no changes, + # do not loop forever. + if approved_count == 0: + result["approval"] = { + "approved_count": total_approved, + "batches": approval_batches, + "error": ( + "SuperDocs requested approval but returned " + "no pending changes." + ), + } + + result["after"] = { + "status": "superdocs_failed", + "error": ( + "SuperDocs requested approval but returned " + "no pending changes." + ), + } + + return result + + # Wait for SuperDocs to continue processing. + job_result = wait_for_job( + client, + job_id, + ) + + result["approval"] = { + "approved_count": total_approved, + "batch_count": len(approval_batches), + "batches": approval_batches, + } + + result["job_after_approval"] = job_result + + # --------------------------------------------------------- + # 6. Handle failed/cancelled jobs + # --------------------------------------------------------- + + if job_result.get("status") != "completed": + result["after"] = { + "status": "superdocs_failed", + "error": job_result.get("error"), + "job_status": job_result.get("status"), + } + + return result + + # --------------------------------------------------------- + # 7. Extract final HTML + # --------------------------------------------------------- + + updated_html = ( + job_result + .get("result", {}) + .get("document_changes", {}) + .get("updated_html", "") + ) + + result["updated_html"] = updated_html + + if not updated_html: + result["after"] = { + "status": "superdocs_no_output", + "error": "SuperDocs returned no updated HTML.", + } + + return result + + # --------------------------------------------------------- + # 8. Re-check page limit + # --------------------------------------------------------- + + updated_text = _html_to_text( + updated_html + ) + + after_check = enforce_page_limit_locally( + document_type, + updated_text, + page_limit, + ) + + result["edited"] = True + result["after"] = after_check + + # --------------------------------------------------------- + # 9. Export final SuperDocs document + # --------------------------------------------------------- + + if after_check["within_limit"]: + result["export"] = _export_superdocs_document( + client=client, + session_id=session_id, + updated_html=updated_html, + document_path=document_path, + ) + else: + result["export"] = { + "exported": False, + "reason": ( + "Final document still exceeds the page limit." + ), + } + + return result + + +def run_superdocs_workflow( + grant_directory: str | Path, +) -> Dict[str, Any]: + """ + Run the complete local grant + SuperDocs workflow. + """ + + grant_directory = Path( + grant_directory + ) + + # --------------------------------------------------------- + # 1. Build and validate grant package + # --------------------------------------------------------- + + assembly = run_grant_assembly( + grant_directory + ) + + validation = assembly["validation"] + + # Never send an invalid grant package to SuperDocs. + if not validation["valid"]: + return { + "status": "validation_failed", + "validation": validation, + "grant_package": assembly["grant_package"], + "output_path": assembly["output_path"], + } + + # --------------------------------------------------------- + # 2. Create SuperDocs client + # --------------------------------------------------------- + + client = SuperDocsClient() + + # --------------------------------------------------------- + # 3. Run research-narrative page-limit workflow + # --------------------------------------------------------- + + research_narrative_path = ( + grant_directory + / "research_narrative.txt" + ) + + page_limit_result = run_page_limit_workflow( + client=client, + document_path=research_narrative_path, + document_type="research_narrative", + page_limit=5, + ) + + return { + "status": "completed", + "validation": validation, + "grant_package": assembly["grant_package"], + "biosketches": assembly.get( + "biosketches", + [], + ), + "output_path": assembly["output_path"], + "page_limit": page_limit_result, + } \ No newline at end of file diff --git a/use-cases/Anirudh7S/research-grant-packet-assembler/llms-full.txt b/use-cases/Anirudh7S/research-grant-packet-assembler/llms-full.txt new file mode 100644 index 00000000..b9d792fb --- /dev/null +++ b/use-cases/Anirudh7S/research-grant-packet-assembler/llms-full.txt @@ -0,0 +1,8447 @@ +# Account Setup +Source: https://docs.superdocs.app/account/account-setup + +Create a SuperDocs account, verify your email, and manage your profile. + +# Account Setup + +## Create an account + +Go to [use.superdocs.app](https://use.superdocs.app) and sign up with one of two methods: + +* **Google Sign-In** GÇö One click, no password needed +* **Email and password** GÇö Enter your name, email, and a password (6+ characters) + +Both methods create a free account with 500 operations per month. + +## Email verification + +If you signed up with email/password, a verification email is sent automatically. Click the link in the email to verify. A banner in the app reminds you until verification is complete. + +## Profile management + +Click your avatar in the top-right corner to view your profile. You can update your display name and see your usage statistics. + +## Password reset + +1. Click **Log In** on the app +2. Click **Forgot password?** +3. Enter your email address +4. Check your inbox for the reset link +5. Set a new password + +## Next steps + + + Create an API key to use SuperDocs programmatically + + + +# API Keys +Source: https://docs.superdocs.app/account/api-keys + +Create, manage, and revoke API keys for programmatic access to SuperDocs. + +# API Keys + +API keys let you access SuperDocs from your code, scripts, or AI tools. + +## Create a key + +1. Open [use.superdocs.app](https://use.superdocs.app) and sign in +2. Click the **gear icon** to open Settings +3. Go to the **API Keys** tab +4. Click **Create API Key** +5. Enter a name (e.g., "My App" or "CI/CD") +6. **Copy the key immediately** GÇö it's only shown once + +## Key format + +Keys start with `sk_` followed by 32 hex characters: + +``` +sk_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4 +``` + +## Authentication + +Include your key in the `Authorization` header: + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/chat \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"message": "Hello", "session_id": "test"}' +``` + +The same header format works for both REST API and MCP connections. + +## Verify your key + +The cheapest way to confirm a key works is `GET /v1/sessions` GÇö it consumes no operations, returns immediately, and accepts API keys: + +```bash theme={null} +curl https://api.superdocs.app/v1/sessions \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +A `200` response with a JSON list of your sessions (possibly empty on a brand-new key) confirms the key is valid. A `401` with `{"detail": "Authentication required..."}` means the key is wrong, revoked, or the `Authorization` header didn't reach the server. + + + **Do not use `/v1/users/me` (or any `/v1/users/*` endpoint) to verify your API key.** Those endpoints are for the web-app session surface GÇö they accept web-app session tokens only and reject `sk_` keys with a `401`. The error makes it look like your key is bad; it isn't, you're just hitting an endpoint that doesn't accept API keys. + + Endpoints that accept API keys: `/v1/chat`, `/v1/chat/async`, `/v1/sessions`, `/v1/sessions/{id}/history`, `/v1/sessions/{id}/jobs`, `/v1/jobs`, `/v1/attachments/*`, `/v1/templates`, `/v1/documents/*`, `/v1/chat/{sid}/approve`. Anything under `/v1/users/*` does not. + + +## Key limits + +| Plan | Max keys | +| --------------------- | -------- | +| Free | 5 | +| Plus, Pro, Enterprise | 25 | + +## Security + +* Keys are **shown once** at creation. Store them securely. +* Keys are stored as hashed values GÇö we cannot recover a lost key. +* **Revoke** a key anytime from Settings > API Keys. Revocation is permanent. +* Never commit keys to source control. Use environment variables. + +## Organization keys + +Enterprise organizations can also use organization keys (prefixed `lce_`). These work identically GÇö same `Authorization: Bearer` header, same endpoints. Organization keys are provisioned for B2B customers. + +## Manage keys + +Create, list, and revoke keys from **Settings > API Keys** in [use.superdocs.app](https://use.superdocs.app). Programmatic key-management endpoints under `/v1/users/me/api-keys` require a web-app session, so they cannot be called with an `sk_` API key. + +If you are an AI agent and need a key with no human in the loop, use the [agent signup endpoint](/introduction/agent-signup) instead: one call to `POST /v1/agents/signup` returns a working `sk_` key on the normal free tier (500 ops/month). Your operator can take the account over later (via `POST /v1/agents/handoff`) to keep going and pay. + + +# Billing +Source: https://docs.superdocs.app/account/billing + +Upgrade your plan, manage your subscription, and cancel from the SuperDocs Settings page. + +# Billing + +All billing management happens through the SuperDocs web app. No API calls needed. + +## Upgrade your plan + +1. Open [use.superdocs.app](https://use.superdocs.app) and sign in +2. Click the **gear icon** to open Settings +3. Go to the **Billing** tab +4. Click **Upgrade** next to the plan you want +5. Complete checkout on the payment page +6. Your plan activates immediately + +## Manage your subscription + +Click **Open Billing Portal** in Settings > Billing to: + +* View invoices +* Update your payment method +* Change your plan + +## Cancel your subscription + +1. Go to Settings > Billing +2. Click **Cancel Subscription** +3. Confirm in the dialog + +After cancellation: + +* Your current plan stays active until the end of the billing period +* When the period ends, your account reverts to the free plan (500 ops/month) +* You can reactivate before the period ends to keep your plan + + +# MCP Setup +Source: https://docs.superdocs.app/account/mcp-setup + +Generate an MCP key from the SuperDocs Settings page and connect your AI tool. + +# MCP Setup + +The MCP tab in Settings generates a dedicated API key and shows connection details for your AI tool. + + + This page is just about **getting a key from the app**. For the protocol overview, the endpoint, and per-client config, see [MCP Overview](/mcp/setup) and the client guides it links (Claude Code, Cursor & VS Code, Claude Desktop). You don't need both pages. Start with the one that matches where you are. + + +## Generate your MCP key + +1. Open [use.superdocs.app](https://use.superdocs.app) and sign in +2. Click the **gear icon** to open Settings +3. Go to the **MCP** tab +4. Click **Generate MCP Key** + +A dedicated API key named "MCP Integration" is created. The connection details appear: + + + The "MCP Key" is a regular API key with the name "MCP Integration" GÇö same format, same auth, same usage limits as keys you create in the [API Keys](/account/api-keys) tab. The MCP tab is just a convenience that auto-names the key and pre-fills it into the config snippets below. You can also use any `sk_` key from the API Keys tab here GÇö they work identically. + + +| Field | Value | +| ----------------- | ------------------------------------ | +| **Endpoint** | `https://api.superdocs.app/mcp/` | +| **Transport** | Streamable HTTP | +| **Authorization** | `Bearer sk_...` (your generated key) | + +Both `/mcp` and `/mcp/` are accepted; the trailing-slash form is the canonical spelling. + +## Connect your AI tool + +Copy the config snippet shown in Settings and paste it into your tool's configuration file. + +For step-by-step setup instructions, see the MCP Integration guides: + + + + JSON config for Claude Desktop + + + + Config for Cursor, VS Code, Windsurf + + + + CLI command for Claude Code + + + + +# Plans & Usage +Source: https://docs.superdocs.app/account/plans-and-usage + +Subscription tiers, operation limits, what counts as an operation, and how to track usage. + +# Plans & Usage + +All plans include full access to the web app, REST API, MCP, and all AI model tiers. Plans differ only by operation volume. + +## Pricing tiers + +| Plan | Price | Included operations | Overage | +| -------------- | ------- | ------------------- | -------------------------------------------------- | +| **Free** | \$0/mo | 500/mo | Pauses at limit (429 error) | +| **Plus** | \$20/mo | 2,000/mo | Pay-as-you-go up to 4,000 total. Max bill \$50/mo. | +| **Pro** | \$99/mo | 10,000/mo | Pay-as-you-go, no cap | +| **Enterprise** | Custom | Custom | Custom | + +## What counts as an operation + +AI actions that modify, analyze, or search documents count as operations: + +* Editing a document section +* A multi-section edit in one request (e.g. "tighten every section") counts as **one** operation; very large requests count one operation per 25 sections edited +* Creating new content +* Opening a brand-new document via chat ("put this in a new document") +* Deleting content +* Searching the document +* Searching attachments +* Analyzing document context +* Loading a template + +**Not counted:** Opening the editor, typing, formatting text, viewing sessions, basic conversations without document actions GÇö and review-mode changes you **deny** are not billed. Edit requests that **fail** bill 0, and so does an edit request the document **already satisfies** (the AI replies honestly that nothing needed changing instead of charging you for a rewrite). + +## Check your usage + +### In the app + +Click your avatar to see your usage bar, operations count, and remaining operations. + +### In API responses + +Every chat response includes a `usage` object: + +```json theme={null} +{ + "usage": { + "monthly_used": 42, + "monthly_limit": 500, + "monthly_remaining": 458, + "was_billable": true, + "ops_charged": 1, + "quota_exhausted": false, + "subscription_tier": "free" + } +} +``` + +`ops_charged` is how many operations the request billed GÇö a single request can bill more than one (one operation per 25 sections edited), so `monthly_used` can increase by more than 1 between responses. `quota_exhausted` becomes `true` when you have reached your plan's operation limit with no remaining balance; the current request still completes, but further billable requests pause until you upgrade or your billing cycle resets. + + + **Reading usage from an API key:** the `/v1/users/me/usage` and `/v1/users/me/limits` endpoints belong to the web-app account surface and accept web-app session tokens only GÇö they reject `sk_` / `lce_` API keys with a `401`. From an API-key context, read usage straight off the `usage` block returned in **every** `/v1/chat` and `/v1/chat/async` response (shown above) and the SSE `usage` event GÇö no separate call needed. View usage in the browser at [use.superdocs.app](https://use.superdocs.app) GåÆ Settings. + + +## Promo codes & credits + +If you have a promo code, redeem it for operation credits. Credits are drawn down **before** your plan's monthly allowance, so they stretch your included operations further. + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/promo/redeem \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"code": "YOUR_CODE"}' +``` + +Redemption is for **personal accounts** GÇö authenticate with a personal API key (`sk_`) or a logged-in session. Organization keys (`lce_`) can't redeem codes. Your account email must be verified, and each code can be redeemed **once per account**. + +Check your active credits: + +```bash theme={null} +curl https://api.superdocs.app/v1/users/me/promotions \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +This returns your active promotions and their remaining credit balance. + +## Monthly reset + +Operations reset at the start of each billing cycle. Free plans reset monthly from account creation. + + +# Agent Adopt +Source: https://docs.superdocs.app/api-reference/agents/agent-adopt + +/openapi.json post /v1/agents/adopt +Human takes over an agent account IN PLACE. Auth = the human's signed-in identity token. + +Called by the web app's adopt page after the human signs in. Verifies the +human's signed-in identity (WITHOUT the get-or-create middleware, to avoid +minting a duplicate row), then re-points the agent account row to that +identity GÇö `users.id` is preserved, so the agent's documents, usage, and API +key all stay. If the human already has an account, returns 409. + + + +# Agent Adopt Info +Source: https://docs.superdocs.app/api-reference/agents/agent-adopt-info + +/openapi.json get /v1/agents/adopt-info +Public, token-keyed context for the adopt page: WHICH agent set this account up, +where it runs, and where to find the takeover code. Recognition / anti-phishing only. +NEVER returns the code itself; the unguessable token is the gate. + + + +# Agent Challenge +Source: https://docs.superdocs.app/api-reference/agents/agent-challenge + +/openapi.json get /v1/agents/challenge +Get a proof-of-work challenge for signup (only needed when the challenge gate is enabled). + +Solve it headlessly with no browser: brute-force an integer `n` in +[0, maxNumber] such that `SHA-256(salt + str(n))` equals `challenge`, then +base64-encode `{algorithm, challenge, number:n, salt, signature}` and send it +as the `altcha` field in POST /v1/agents/signup. + + + +# Agent Handoff +Source: https://docs.superdocs.app/api-reference/agents/agent-handoff + +/openapi.json post /v1/agents/handoff +Email your human a one-time link to ADOPT this account (take ownership + pay). + +Call with your `sk_` key. Pass `working_context` (e.g. "the ~/Documents/acme +project on your Mac") so the email is recognizable and not mistaken for spam. + +The response includes a short **takeover_code** (like ABCD-1234). You MUST show +this code to your human operator GÇö they enter it after opening the emailed link +and signing in. It is the security check that stops anyone who merely received +the email (a wrong address, or a prompt-injected one) from taking the account. +Tell your operator the code directly (you may also save it locally, e.g. +~/.superdocs/takeover-code.txt); never email it or post it anywhere external. + +The human opens the link, signs in, enters the code, and the account becomes +theirs in place GÇö you keep all your work and this API key keeps working. + + + +# Agent Request Upgrade +Source: https://docs.superdocs.app/api-reference/agents/agent-request-upgrade + +/openapi.json post /v1/agents/request-upgrade +For a FULLY AUTONOMOUS agent with no human who can upgrade: register interest in paying directly. + +We don't offer agent-native (no-human) payment yet, but we're gauging demand. +Call this with your `sk_` key when you've hit your cap and genuinely cannot +involve a human. We record it and follow up (or reach hello@superdocs.app). +If a human CAN help, use POST /v1/agents/handoff instead GÇö that upgrades you now. + + + +# Agent Signup +Source: https://docs.superdocs.app/api-reference/agents/agent-signup + +/openapi.json post /v1/agents/signup +Create a SuperDocs account and get a working API key in one call GÇö no human required. + +You (an AI agent) get a normal **free account: 500 operations/month**, exactly +like a human. Use the returned `sk_` key as `Authorization: Bearer ` on +this API or the MCP server at `/mcp`. Store the key GÇö it is shown only once. +Save the whole response to `~/.superdocs/agent_credentials.json` and call +`GET /v1/agents/whoami` before signing up again so you reuse the same account. +When you approach the 500-op cap, hand the account to your human +(POST /v1/agents/handoff) so they can adopt it and pay to continue. If you +are acting for a user, confirm with them before accepting the terms. + + + +# Check your account status: subscription tier plus monthly operations used and remaining. +Source: https://docs.superdocs.app/api-reference/agents/check-your-account-status:-subscription-tier-plus-monthly-operations-used-and-remaining + +/openapi.json get /v1/agents/whoami +Check your SuperDocs account status: tier, monthly operations used/remaining, and (for agent accounts) whether a human has adopted it. + +Call this with your `sk_` key. Useful before doing work (to confirm you have +operations left) and before signing up again (to reuse your account). + +Nearing your monthly cap and need to keep going? Hand the account to a human: +POST /v1/agents/handoff with their email returns a one-time takeover code to +give them; they open the emailed link, enter the code, and adopt the account +in place (your work and this API key keep working) so they can upgrade and +pay. No human? POST /v1/agents/request-upgrade instead. Full flow: +https://docs.superdocs.app/introduction/agent-signup + + + +# Api Catalog +Source: https://docs.superdocs.app/api-reference/api-catalog + +/openapi.json get /.well-known/api-catalog + + + +# Health Check Deep +Source: https://docs.superdocs.app/api-reference/health-check-deep + +/openapi.json get /health/deep +Deep health check that verifies the job queue, AI workflow state store, and database are all responsive within a bounded time budget. Used by container orchestrators as a liveness probe to distinguish "process alive and TCP listening" (what /health reports) from "application is actually serving requests" (this endpoint). + + + +# Mcp Server Card +Source: https://docs.superdocs.app/api-reference/mcp-server-card + +/openapi.json get /.well-known/mcp.json + + + +# Mcp Server Card +Source: https://docs.superdocs.app/api-reference/mcp-server-card-1 + +/openapi.json get /.well-known/mcp-server-card + + + +# Verify the SuperDocs MCP server is reachable and serving traffic. +Source: https://docs.superdocs.app/api-reference/mcp/verify-the-superdocs-mcp-server-is-reachable-and-serving-traffic + +/openapi.json get /health +Returns 200 with {"status":"healthy"} when the API is up. Call this once after MCP install to confirm the connection works before invoking other tools. No authentication required. + + + +# Oauth Authorization Server +Source: https://docs.superdocs.app/api-reference/oauth-authorization-server + +/openapi.json get /mcp/.well-known/oauth-authorization-server + + + +# Oauth Authorization Server +Source: https://docs.superdocs.app/api-reference/oauth-authorization-server-1 + +/openapi.json get /.well-known/oauth-authorization-server/mcp + + + +# Oauth Authorization Server +Source: https://docs.superdocs.app/api-reference/oauth-authorization-server-2 + +/openapi.json get /.well-known/oauth-authorization-server + + + +# Oauth Protected Resource +Source: https://docs.superdocs.app/api-reference/oauth-protected-resource + +/openapi.json get /mcp/.well-known/oauth-protected-resource + + + +# Oauth Protected Resource +Source: https://docs.superdocs.app/api-reference/oauth-protected-resource-1 + +/openapi.json get /.well-known/oauth-protected-resource/mcp + + + +# Oauth Protected Resource +Source: https://docs.superdocs.app/api-reference/oauth-protected-resource-2 + +/openapi.json get /.well-known/oauth-protected-resource + + + +# Openid Configuration +Source: https://docs.superdocs.app/api-reference/openid-configuration + +/openapi.json get /mcp/.well-known/openid-configuration + + + +# Openid Configuration +Source: https://docs.superdocs.app/api-reference/openid-configuration-1 + +/openapi.json get /.well-known/openid-configuration/mcp + + + +# Openid Configuration +Source: https://docs.superdocs.app/api-reference/openid-configuration-2 + +/openapi.json get /.well-known/openid-configuration + + + +# List My Promotions +Source: https://docs.superdocs.app/api-reference/promo/list-my-promotions + +/openapi.json get /v1/users/me/promotions +List the authenticated user's promotion redemptions, split into `active` (drawable now) +and `history` (exhausted / expired / revoked). + +Used by the Settings GåÆ Billing tab to render the Credits & Promotions section. + + + +# Redeem Promo +Source: https://docs.superdocs.app/api-reference/promo/redeem-promo + +/openapi.json post /v1/promo/redeem +Redeem a promo code and add the granted operations to the authenticated user's account. + +B2C only: web app login tokens and sk_ user API keys are accepted; lce_ org keys are rejected. +Requires a verified email. Each user may redeem a given code at most once. + +Rate-limited to 3 attempts per IP per hour. All attempts (success and failure) are +logged for audit. + + + +# Root +Source: https://docs.superdocs.app/api-reference/root + +/openapi.json get / + + + +# Get a pre-signed URL to download an exported document without proxying through the agent. +Source: https://docs.superdocs.app/api-reference/uploads/get-a-pre-signed-url-to-download-an-exported-document-without-proxying-through-the-agent + +/openapi.json post /v1/downloads +Returns a short-lived (15-minute) GET URL plus a ready-to-run `curl_example`. An agent with shell access can run the `curl_example` directly to save the exported file to the working directory, so the bytes never pass through its context window; clients without shell execution (or callers who only want the link) can use the signed URL directly. Generates the document in the requested format and returns a time-limited signed download URL. Specify format as 'pdf', 'docx', 'html', 'markdown', or 'txt' (legacy 'doc' also accepted). + + + +# Get a pre-signed URL to upload large files (.docx/PDF/HTML/MD/RTF) without bloating agent context. +Source: https://docs.superdocs.app/api-reference/uploads/get-a-pre-signed-url-to-upload-large-files-docxpdfhtmlmdrtf-without-bloating-agent-context + +/openapi.json post /v1/uploads +Returns a short-lived (5-minute) PUT URL plus a ready-to-run `curl_example`. An agent with shell access can run the `curl_example` directly to push the file to cloud storage, so the bytes never pass through its context window; clients without shell execution can surface the command or URL for the caller to run. After upload completes, call process_uploaded_document with the upload_id to trigger parsing. For files <100KB where token cost is trivial, upload_document_base64 still works inline. Max file size: 100 MB. Supported formats: .pdf, .docx, .txt, .rtf, .md, .html, .htm, .tex (plus .zip LaTeX project archives). + + + +# Parse an uploaded file into structured HTML with chunk IDs for targeted AI editing. +Source: https://docs.superdocs.app/api-reference/uploads/parse-an-uploaded-file-into-structured-html-with-chunk-ids-for-targeted-ai-editing + +/openapi.json post /v1/uploads/{upload_id}/process +Fetches the file uploaded via request_upload_url and runs the same parsing pipeline as upload_document_base64: every paragraph, heading, table, row, and cell gets a unique chunk ID, enabling targeted structural edits via chat ("remove row 3 of the pricing table" works), with tables, borders, shading, alternating row colors, fonts, and inline styling preserved on edit and export. By default the response is compact (metadata only, no document body) to keep your context small GÇö the document is loaded into the session and you edit it via chat; pass return_html=true if you need the parsed HTML inline. Uploading and parsing is NOT itself a billable operation GÇö you are charged only when the AI edits the document. Specify parse_mode='document' to load as the active editable document, or parse_mode='attachment' to load as a read-only AI-searchable reference. + + + +# Check User Limits +Source: https://docs.superdocs.app/api-reference/users/check-user-limits + +/openapi.json get /v1/users/me/limits +Check current usage limits and remaining operations + +Returns real-time usage information with reset date + + + +# Create Api Key +Source: https://docs.superdocs.app/api-reference/users/create-api-key + +/openapi.json post /v1/users/me/api-keys +Create a new API key for the current user. + +The raw key is returned ONCE in the response. It cannot be retrieved again. + + + +# Delete User Session +Source: https://docs.superdocs.app/api-reference/users/delete-user-session + +/openapi.json delete /v1/users/me/sessions/{session_id} +Delete a specific chat session + +Removes all messages for the given session ID (user must own the session) + + + +# Get Current User Profile +Source: https://docs.superdocs.app/api-reference/users/get-current-user-profile + +/openapi.json get /v1/users/me +Get current user's profile information + +Returns user profile with subscription tier and usage limits + + + +# Get User Sessions +Source: https://docs.superdocs.app/api-reference/users/get-user-sessions + +/openapi.json get /v1/users/me/sessions +List all chat sessions for current user + +Returns list of sessions with last activity and message counts + + + +# Get User Usage Stats +Source: https://docs.superdocs.app/api-reference/users/get-user-usage-stats + +/openapi.json get /v1/users/me/usage +Get detailed usage statistics for current user + +Returns operation counts, token usage, and success rates by operation type + + + +# List Api Keys +Source: https://docs.superdocs.app/api-reference/users/list-api-keys + +/openapi.json get /v1/users/me/api-keys +List all API keys for the current user (masked). + +Returns key prefix and last 4 characters for identification. + + + +# Revoke Api Key +Source: https://docs.superdocs.app/api-reference/users/revoke-api-key + +/openapi.json delete /v1/users/me/api-keys/{key_id} +Revoke (soft delete) an API key. + +The key will be marked as inactive and can no longer be used for authentication. + + + +# Update User Profile +Source: https://docs.superdocs.app/api-reference/users/update-user-profile + +/openapi.json patch /v1/users/me +Update current user's profile + +Allows updating display name, timezone, language, and preferences + + + +# Approve or deny AI-proposed document changes one-by-one or in batch (HITL workflow). +Source: https://docs.superdocs.app/api-reference/v1/approve-or-deny-ai-proposed-document-changes-one-by-one-or-in-batch-hitl-workflow + +/openapi.json post /v1/chat/{session_id}/approve +Used with chat_async when approval_mode='ask_every_time'. For each proposed change (with chunk_id and HTML diff), respond approved=true|false plus optional feedback for the AI to revise on. Approved changes apply atomically; denied changes are discarded; the AI may revise based on feedback in the next turn. Required for regulated workflows (legal, medical, compliance) where every AI edit must be reviewed before it touches the document. For single changes: set approved=true/false and optionally provide feedback. For batch decisions: provide a 'changes' array with per-change decisions. After approval, the job resumes processing and eventually reaches status=completed. + + + +# Cancel a pending or in-progress async chat job. +Source: https://docs.superdocs.app/api-reference/v1/cancel-a-pending-or-in-progress-async-chat-job + +/openapi.json post /v1/jobs/{job_id}/cancel +Stops the AI mid-edit. Already-applied changes are preserved in the document; pending changes are discarded. Use to abort long-running operations that are no longer needed (e.g., user changed their mind, or you want to retry with different parameters or model_tier). Only jobs with status pending or processing can be cancelled. Returns the updated job details with status cancelled. + + + +# Check processing status of all attachments in a session. +Source: https://docs.superdocs.app/api-reference/v1/check-processing-status-of-all-attachments-in-a-session + +/openapi.json get /v1/attachments/status/{session_id} +Returns each attachment's processing state (pending/processing/completed/failed) plus extracted text length and chunk count once ready. Poll this after upload_attachment_base64 to know when an attachment becomes queryable by the AI. Surfaces processing errors with actionable messages. + + + +# Clear your cross-session memory note. +Source: https://docs.superdocs.app/api-reference/v1/clear-your-cross-session-memory-note + +/openapi.json delete /v1/users/me/cross-session-memory +Delete the caller's cross-session memory note. Owner-scoped: a caller can only clear its OWN +note. With no key this clears the account-level note; with a memory_key it clears that one +end-customer's note. Idempotent (removed=0 if it didn't exist). + + + +# Close (remove) a document from a multi-document session. +Source: https://docs.superdocs.app/api-reference/v1/close-remove-a-document-from-a-multi-document-session + +/openapi.json delete /v1/sessions/{session_id}/documents/{document_id} +Remove document_id from the session's open documents (the tab close button). If it's the +focused document, focus another open document GÇö `next_focus` if it's still open (so the client +can pick the adjacent tab), else the next remaining one; closing the last document leaves the +session empty. Returns the updated document roster + the now-focused document's HTML. + +The close takes effect immediately and is persisted, so a later reconnect or session restore +won't bring the closed document back. Persistence is best-effort GÇö a failure is logged, not +fatal (the close still holds for the active session). + + + +# Delete a saved document template by ID. +Source: https://docs.superdocs.app/api-reference/v1/delete-a-saved-document-template-by-id + +/openapi.json delete /v1/templates/{template_id} +Soft-deletes the template; only the owner (user or organization) can delete. Once deleted, the template no longer appears in list_user_templates and cannot be referenced by the AI for new documents. Existing documents already drafted from the template are unaffected. + + + +# Delete (archive) a saved document. +Source: https://docs.superdocs.app/api-reference/v1/delete-archive-a-saved-document + +/openapi.json delete /v1/documents/{document_id} +Soft-archive a saved document GÇö it leaves the Files view but is recoverable server-side. +Presence-aware: if the document is currently open in ANOTHER session and `force` is not set, +the request FAILS honestly with HTTP 409 (`code:"document_in_use"` + `open_in_sessions:N` + +`suggested_action`) so callers can confirm "open in N other session(s) GÇö delete anyway?" and +re-call with `force=true`. On `force=true` it archives, unlinks every session, and notifies the +other sessions (which then prompt the user to keep editing or honor the deletion). + +A no-op never wears a success status: if nothing was archived you get the 409 above, so a +2xx always means the archive actually happened. + + + +# Edit, draft, or restructure a document using natural language. Preserves tables, styling, and formatting. +Source: https://docs.superdocs.app/api-reference/v1/edit-draft-or-restructure-a-document-using-natural-language-preserves-tables-styling-and-formatting + +/openapi.json post /v1/chat +Synchronous AI chat that can rewrite specific paragraphs, add or remove table rows, restructure sections, generate new content from templates, or transform an entire document. Pass document_html only to load or replace the document; once a session holds a document the server persists it across turns, so omit it on follow-up turns. To replace the whole document with EXACT content you already hold, pass that content in document_html (a verbatim load GÇö the AI never re-types it) or upload it via upload_document_base64; describing the content only in `message` makes the AI author it, which can drift from your text. Returns AI response text plus structural document changes (HTML edits, additions, deletions) with chunk IDs. One billable operation per document-modifying turn; very large multi-section edits bill one operation per 25 sections changed. For long-running edits or human-in-the-loop approval, use chat_async. Optional: model_tier (core/turbo/pro/max), thinking_depth (fast/balanced/deep), image_attachments for multimodal vision. RECOMMENDED for AI agents working with large documents (>20 pages): set response_mode='compact' to skip the full HTML in the response (the AI returns only per-section diffs in chunk_diffs) and save thousands of tokens per turn. To read sections in compact mode, just send a natural-language request like 'show me the force majeure section' GÇö the AI returns the content in the reply text. Always use natural language to describe what you want; the AI handles all internal section lookups. + + + +# Export the current document as a styled .docx (default), .pdf, .html, .md, or .txt file with full fidelity. +Source: https://docs.superdocs.app/api-reference/v1/export-the-current-document-as-a-styled-docx-default-pdf-html-md-or-txt-file-with-full-fidelity + +/openapi.json post /v1/documents/export +Round-trips the document through the original docx renderer, preserving tables, borders, shading, alternating row colors, headers, footers, fonts, inline styling, and embedded images. Two modes: pass html directly to export ad-hoc content, OR pass session_id to export the session's current state. Format options: 'docx' (default, native Open XML, best for programmatic processing or mail merge), 'pdf', 'html', 'markdown', or 'txt' ('doc' is a legacy Word-compatible HTML alias). Fidelity is the differentiator vs naive HTML to docx converters. + + + +# Get a saved document's detail, structure outline, and the chats that used it. +Source: https://docs.superdocs.app/api-reference/v1/get-a-saved-documents-detail-structure-outline-and-the-chats-that-used-it + +/openapi.json get /v1/documents/{document_id} +One saved document's metadata plus a STRUCTURE outline and the chat sessions that have it +open (prior-chats-per-file). Owner-scoped GÇö 404 if the document isn't yours. + +`structure` (always included, token-light, NON-BILLABLE) is the cheap verify step after +any edit: `headings` (level + text + position), `section_count` (heading-anchored), `block_count` +(editable chunks), `media` (image/diagram counts). It answers "did my edit land?" for free GÇö +never export a whole document just to check its structure. Pass include_html=true only when +you need the body. + + + +# Get the status, partial results, and any pending changes for an async chat job. +Source: https://docs.superdocs.app/api-reference/v1/get-the-status-partial-results-and-any-pending-changes-for-an-async-chat-job + +/openapi.json get /v1/jobs/{job_id} +Returns the job's current state (pending, in_progress, awaiting_approval, completed, failed, or cancelled), intermediate AI responses streamed during execution, the final document HTML once complete, and any pending changes awaiting user approval (with chunk IDs and proposed HTML diffs in metadata.pending_changes). Poll this after chat_async to track progress and retrieve results. When status is awaiting_approval, call POST /v1/chat/{session_id}/approve to approve or deny each pending change. Headless/MCP clients get the same typed progress events the web SSE stream delivers via metadata.intermediate_responses (e.g. documents_changed, continue_prompt, model_fallback, proposed_change_batch); cross-session document updates from OTHER sessions are reported separately by polling GET /v1/sessions/{session_id}/doc-events. + + + +# List all async chat jobs for a specific session, most recent first. +Source: https://docs.superdocs.app/api-reference/v1/list-all-async-chat-jobs-for-a-specific-session-most-recent-first + +/openapi.json get /v1/sessions/{session_id}/jobs +Returns the full job history of one document. Useful for auditing what the AI did to a document over time, or finding a specific job that's waiting for HITL approval. Same shape as list_jobs but scoped to one session. + + + +# List all saved document templates available to the user or organization. +Source: https://docs.superdocs.app/api-reference/v1/list-all-saved-document-templates-available-to-the-user-or-organization + +/openapi.json get /v1/templates +Returns active (non-deleted) templates with name, format, size, and creation date metadata. Use to show the AI what reusable document structures are available for drafting new documents. Templates are scoped to the authenticated entity: users via the web app or sk_ key see only their own templates; organizations via lce_ key see theirs. + + + +# List the documents open in a multi-document session. +Source: https://docs.superdocs.app/api-reference/v1/list-the-documents-open-in-a-multi-document-session + +/openapi.json get /v1/sessions/{session_id}/documents +Returns the editable documents open in this session (focused first), each with its id, chunk +count, and focused flag GÇö so a client can render document tabs. The response is token-light by +default; pass include_html=true to also get each document's reassembled HTML. + +ID MAPPING: `document_id` is the SESSION-LOCAL slot id ("doc_primary", "doc_ab12GǪ"); +`durable_document_id` is the PERMANENT documents.id UUID GÇö the SAME id `list_documents` (Files) +shows and `get_document_detail` / `rename_document` / `archive_document` / `open_documents` +take (null until the document's first save). focus_session_document, close_session_document, +and chat's `document_id` accept EITHER form, so you can drive a session entirely with durable +ids. + + + +# List your active document editing sessions to resume or audit prior work. +Source: https://docs.superdocs.app/api-reference/v1/list-your-active-document-editing-sessions-to-resume-or-audit-prior-work + +/openapi.json get /v1/sessions +Returns sessions sorted by most recent activity, with message counts and last-updated timestamps. Each session represents one document with full edit history and AI conversation persisted server-side. Use to find a previous editing context to resume (then call get_session_history) or to audit your workspace. + + + +# List your async chat jobs (in-progress, awaiting approval, completed, failed). +Source: https://docs.superdocs.app/api-reference/v1/list-your-async-chat-jobs-in-progress-awaiting-approval-completed-failed + +/openapi.json get /v1/jobs +Returns jobs sorted by most recent, optionally filtered by status. Use to monitor long-running AI edits started via chat_async, see which jobs are paused waiting for human approval, or audit completed work. Each job tracks the chat that started it, the changes made, and any HITL decisions logged. + + + +# List your saved documents (the Files view). +Source: https://docs.superdocs.app/api-reference/v1/list-your-saved-documents-the-files-view + +/openapi.json get /v1/documents +List the authenticated user/org's saved documents, most-recently-updated first +(metadata only GÇö no document content). Each carries a session_count ("N chats"). Documents +become durable + reusable automatically as you create or edit them; on the FIRST call we also +one-time import the documents of pre-Files-view chats so prior work appears here too. + + + +# Open a new blank document as a tab in the session. +Source: https://docs.superdocs.app/api-reference/v1/open-a-new-blank-document-as-a-tab-in-the-session + +/openapi.json post /v1/sessions/{session_id}/documents/blank +Open a fresh BLANK document as a new focused tab in the session (the tab-strip "+"). +Non-billable GÇö no AI, no upload pipeline; it is saved on first edit. +REST-only (a manual UI affordance, not an MCP tool GÇö agents create documents through chat). + + + +# Open saved documents into a chat session (shared, never copied). +Source: https://docs.superdocs.app/api-reference/v1/open-saved-documents-into-a-chat-session-shared-never-copied + +/openapi.json post /v1/sessions/{session_id}/documents/open +Load one or more SAVED documents into a session as editable tabs. The SAME durable +document is attached (never copied), so edits flow back to the one shared row with +cross-session soft-collaboration. The first listed document is focused. Returns the +refreshed document roster so the client can render tabs immediately. + + + +# Persist a user-edited document (non-AI autosave). +Source: https://docs.superdocs.app/api-reference/v1/persist-a-user-edited-document-non-ai-autosave + +/openapi.json post /v1/sessions/{session_id}/documents/{document_id}/save +Persist a HUMAN-edited document GÇö the editor's debounced autosave + save-on-blur GÇö WITHOUT +an AI turn. Re-indexes the html into the target document (preserving its durable identity so it +UPDATES the same Files entry), then saves it so pure typing is preserved AND other sessions with +the same document open stay in sync. The AI-edit flow is unaffected (autosave saves first; a +later AI result is merged with your saved edits). Non-billable, REST-only (a UI affordance, not an MCP tool). + + + +# Poll Session Updates +Source: https://docs.superdocs.app/api-reference/v1/poll-session-updates + +/openapi.json get /v1/poll/{session_id} +Long-poll for real-time job updates on a session (B2B organizations only). + +Returns immediately if there are recent job updates, or holds the connection +open up to the specified timeout. Use the 'since' parameter to avoid receiving +duplicate updates. + + + +# Re-apply the AI's intended edit on top of the user's current version of one section. +Source: https://docs.superdocs.app/api-reference/v1/re-apply-the-ais-intended-edit-on-top-of-the-users-current-version-of-one-section + +/openapi.json post /v1/sessions/{session_id}/chunks/{chunk_id}/re-edit +Resolve a concurrent-edit conflict on ONE section. When a user edited a section while the AI +was also changing it, this re-applies the AI's intended change on top of the user's current text +(mode="redo"), or blends the user's and AI's versions into one (mode="merge"). Returns only the +rewritten section HTML. Performs one AI edit and counts as one billable operation. + + + +# Remove an attachment from a session or cancel its in-progress processing. +Source: https://docs.superdocs.app/api-reference/v1/remove-an-attachment-from-a-session-or-cancel-its-in-progress-processing + +/openapi.json delete /v1/attachments/{attachment_id} +Removes the attachment from the session; the AI will no longer reference it in subsequent chat turns. If processing is still in progress, also cancels the underlying job. Use to free up context, remove sensitive files mid-session, or replace a stale reference document. The attachment_id can be either the final attachment ID or the job ID (during processing). + + + +# Rename a saved document and/or update its out-of-flow parts (headers, footers, footnotes, endnotes, comments, sections). +Source: https://docs.superdocs.app/api-reference/v1/rename-a-saved-document-andor-update-its-out-of-flow-parts-headers-footers-footnotes-endnotes-comments-sections + +/openapi.json patch /v1/documents/{document_id} +Rename one of your saved documents and/or update its out-of-flow parts. Parts are +the document's out-of-flow content GÇö headers/footers, footnote and endnote bodies, +comments, and per-section page geometry. Every fragment is sanitized on write, writes +are versioned and safe under concurrent editors, and parts revert with the document. +A title-only call behaves exactly as the original rename (response `status:"renamed"`). +Part content lives in the document body and is edited exactly like any other document +content (via chat or the document write endpoints); a legacy `parts` payload returns +400 `parts_moved_to_chunks` pointing you there. + + + +# Request higher document/chat scale limits in one call GÇö no email needed. +Source: https://docs.superdocs.app/api-reference/v1/request-higher-documentchat-scale-limits-in-one-call-GÇö-no-email-needed + +/openapi.json post /v1/limits/increase-request +Request a higher scale limit for your account in ONE call. + +WHEN TO USE: after any 413 with error_code DOCUMENT_TOO_COMPLEX (a single +document crossed the standard per-document page limit) or SESSION_TOO_FULL +(the documents open in one chat crossed the standard per-chat limit). These +are stability limits, not hard caps: the platform supports larger +deployments, and limits are raised per account on request. + +WHAT HAPPENS: the SuperDocs team is notified immediately with your account +identity and the numbers you send; limits are typically expanded within a +day and you are contacted at your account email. No email writing needed GÇö +though hello@superdocs.app also works if you prefer. + +ALTERNATIVES while you wait: split the file into smaller documents, upload +it as an attachment (reference/search, not editing), start another session +for additional documents (init_session), or close documents you no longer +need (close_session_document). + + + +# Request Large Export Email +Source: https://docs.superdocs.app/api-reference/v1/request-large-export-email + +/openapi.json post /v1/documents/export/email-request +Enqueue a large-export job GÇö runs in the background and emails a +secure, time-limited download link when the export finishes. 24h SLA +promised in the email. + + + +# Restore a document another session archived, keeping your current edits (web app). +Source: https://docs.superdocs.app/api-reference/v1/restore-a-document-another-session-archived-keeping-your-current-edits-web-app + +/openapi.json post /v1/sessions/{session_id}/documents/{document_id}/unarchive +Restore (un-archive) a document that was archived, re-linking it to this session and bringing +its content back. `document_id` is the session-local id (same as /save); the durable id is +recovered from the cached document. Idempotent: restoring an already-active (or never-archived) +document is a no-op. Non-billable. + +Two shapes: WITHOUT a body (e.g. an AI agent restoring a document by id) the archived content is +restored as-is; WITH a body carrying the current editor HTML (the web app's "keep editing +(restores it)" choice after another session deleted the document) the document is restored AND the +supplied edits are saved on top, so it converges for everyone. + + + +# Restore the full conversation and document state for a previous session. +Source: https://docs.superdocs.app/api-reference/v1/restore-the-full-conversation-and-document-state-for-a-previous-session + +/openapi.json get /v1/sessions/{session_id}/history +Returns complete message history (user and AI), final document HTML with chunk IDs preserved, attachment list, and editor actions (font, color, alignment changes). Use to continue editing a document you started in a prior session. The AI rehydrates with full context of all prior decisions, chunk IDs, and attachments. Pass include_document_html=false to skip the full document body when you only need the conversation. + + + +# Restore (un-archive) a previously archived document by its id. +Source: https://docs.superdocs.app/api-reference/v1/restore-un-archive-a-previously-archived-document-by-its-id + +/openapi.json post /v1/documents/{document_id}/unarchive +Restore (un-archive) a saved document by its id GÇö the mirror of archive_document. The document +re-enters your Files view and can be opened/edited again (open it into a chat with open_documents). +`document_id` is the same id used by list_documents / archive_document. Idempotent: restoring an +already-active (or unknown) document is a no-op. Non-billable. + + + +# Resume or stop a chat turn paused by a large-edit continue prompt. +Source: https://docs.superdocs.app/api-reference/v1/resume-or-stop-a-chat-turn-paused-by-a-large-edit-continue-prompt + +/openapi.json post /v1/chat/{session_id}/continue +Used with chat_async when a large edit paused to ask whether to keep going. +Poll get_job until status=awaiting_approval AND metadata.awaiting_kind='continue_prompt'; +then POST here with continue=true to resume (the AI picks up where it left off with a +fresh time/step budget) or continue=false to stop (everything applied so far is kept). +The job then resumes/finishes and reaches status=completed. This is NOT the change- +approval endpoint (that is approve_change) GÇö it can only act on a continue-prompt pause. + + + +# Rewind a chat session to before a specific user message GÇö restores both the document and the conversation in one call. +Source: https://docs.superdocs.app/api-reference/v1/rewind-a-chat-session-to-before-a-specific-user-message-GÇö-restores-both-the-document-and-the-conversation-in-one-call + +/openapi.json post /v1/sessions/{session_id}/revert +Rewinds a chat session to the state immediately before a specific user message. The document, conversation history, and supporting context all snap back to that point. The text of the reverted message is returned (compose_text) so the caller can edit and resend it. Messages from the reverted turn forward are soft-archived: hidden from active reads but retained for audit. The original conversation is preserved server-side; the restored conversation becomes the active timeline. Rejects with 409 if the session has a chat job in progress or awaiting approval GÇö wait for the job to settle before reverting. Returns 422 if the message predates the revertability feature. + + + +# Save a document template (NDA, contract, SOP, letterhead) for reuse across sessions. +Source: https://docs.superdocs.app/api-reference/v1/save-a-document-template-nda-contract-sop-letterhead-for-reuse-across-sessions + +/openapi.json post /v1/templates/upload-base64 +Templates persist across sessions and can be referenced by the AI when drafting new documents (e.g., "draft an NDA using my standard template"). Stored at user or organization scope. Ideal for boilerplate, branded letterheads, recurring document structures, or compliance-required templates. Supports the same formats as document upload: .docx, .pdf, .txt, .rtf, .md, .html, .htm. Max 50 MB validated; note the hosted transport layer caps any REQUEST BODY at ~32 MB and rejects larger ones at the gateway with an HTML (not JSON) error GÇö base64 inflates content ~33%, so inline uploads are reliable up to ~20 MB of file bytes; beyond that use request_upload_url (pre-signed flow, up to 100 MB). + + + +# Session Document Events +Source: https://docs.superdocs.app/api-reference/v1/session-document-events + +/openapi.json get /v1/sessions/{session_id}/doc-events +Poll for cross-session document updates: returns `document_updated` events for documents +THIS session holds that OTHER sessions committed since `after_id`. The client polls this +(~every 1-2s while a doc is open) and re-fetches the changed document's content so all open +sessions converge without a refresh. Excludes this session's own writes by default (the web UI +already has its own changes); a REST/MCP integrator can pass `include_own=true` to receive its +own events too. REST-only GÇö a lightweight poll, deliberately NOT an MCP tool and NOT a held +SSE connection (keeps the per-instance connection budget free). + + + +# Start a long-running or HITL-approved AI edit; returns a job_id to poll for results. +Source: https://docs.superdocs.app/api-reference/v1/start-a-long-running-or-hitl-approved-ai-edit;-returns-a-job_id-to-poll-for-results + +/openapi.json post /v1/chat/async +Use instead of chat when (a) the edit is large or multi-step, (b) you need human approval on each proposed change before it applies (set approval_mode='ask_every_time'), or (c) you can't afford to block on a synchronous response. To replace the whole document with EXACT content you already hold, pass that content in document_html (a verbatim load GÇö the AI never re-types it) or upload it via upload_document_base64; describing the content only in `message` makes the AI author it, which can drift from your text. Returns a job_id immediately. Poll get_job to track progress and retrieve the final document. Approve or deny pending changes via approve_change. Job state is durable and survives server restarts. Optional: model_tier (core/turbo/pro/max), thinking_depth (fast/balanced/deep), image_attachments for multimodal vision. RECOMMENDED for AI agents working with large documents (>20 pages): set response_mode='compact' so the job result skips the full HTML and surfaces only per-section diffs in chunk_diffs, saving thousands of tokens per poll. To read sections in compact mode, just send a natural-language chat request ('show me the pricing section') GÇö the AI returns the content in the reply text. Always use natural language; the AI handles all internal section lookups. HITL workflow (approval_mode='ask_every_time'): 1) poll get_job until status=awaiting_approval, 2) read metadata.pending_changes for proposed edits, 3) call approve_change per change, 4) continue polling until status=completed. + + + +# Start a new chat session and open documents into it in one call. +Source: https://docs.superdocs.app/api-reference/v1/start-a-new-chat-session-and-open-documents-into-it-in-one-call + +/openapi.json post /v1/sessions/init +Create a session AND open N saved documents into it in ONE call (the documents are SHARED, +never copied GÇö the same cross-session behavior as /documents/open). The web UI passes its own +session_id so its session model stays consistent; an MCP/API integrator can omit it and the +server mints one GÇö the one-call way to start a session with documents already open. With no +document_ids it just returns a fresh, empty session. The open semantics (first document focused, +durable binding, returned roster) are identical to /documents/open. + + + +# Stream Chat Progress +Source: https://docs.superdocs.app/api-reference/v1/stream-chat-progress + +/openapi.json get /v1/chat/{session_id}/stream +Stream real-time progress for a chat job using Server-Sent Events (SSE). + +Opens an SSE connection that streams events as the AI processes your request. +Use this with the job_id returned from POST /v1/chat/async. + +Event types: +- 'intermediate': Progress updates during processing (content, sequence, timestamp). +- 'proposed_change_batch': The batch of document changes proposed for review, delivered as one + event carrying changes[] (emitted when approval_mode is 'ask_every_time'). Changes are always + delivered as a batch via this event. +- 'document_sync': Chunk-id sync emitted before the agent runs, so changes can reference stable ids. +- 'continue_prompt': A pause on a large edit, asking whether to continue or stop. +- 'documents_changed': Signals that one or more documents were auto-applied (with per-document + change counts and changed chunk ids). +- 'model_fallback': Notice that the request automatically failed over to another model tier. +- 'final': Processing complete. Contains the full result with AI response and document changes. +- 'usage': Billing data emitted after 'final' (monthly_used, monthly_limit, monthly_remaining). +- 'error': An error occurred (job failed, cancelled, or not found). + +Authentication: Pass a token or api_key as a query parameter (required for EventSource which cannot set headers). + + + +# Switch which document is focused in a multi-document session. +Source: https://docs.superdocs.app/api-reference/v1/switch-which-document-is-focused-in-a-multi-document-session + +/openapi.json post /v1/sessions/{session_id}/documents/{document_id}/focus +Make document_id the focused document (the editor's active document and the default edit +target). Accepts either the session slot id OR the durable documents.id UUID (the id shown by +list_documents/Files). Persists the outgoing focused document into the session's +document map and returns the now-focused document's HTML. + + + +# Undo a revert: restore the pre-revert state and the rolled-back turns. +Source: https://docs.superdocs.app/api-reference/v1/undo-a-revert:-restore-the-pre-revert-state-and-the-rolled-back-turns + +/openapi.json post /v1/sessions/{session_id}/redo +Revert is non-destructive: this restores the session FORWARD to the pre-revert state +(returned by the revert as redo_checkpoint_id), restores the document to that state while still +merging any concurrent edits from other sessions (keeping both where they conflict), and +un-archives exactly the chat turns the revert hid. Intended for use immediately after a revert; +once a new message is sent the timeline diverges and the web app stops offering it. + + + +# Upload a base64-encoded image and get back a stable URL to embed in a document via . +Source: https://docs.superdocs.app/api-reference/v1/upload-a-base64-encoded-image-and-get-back-a-stable-url-to-embed-in-a-document-via- + +/openapi.json post /v1/documents/images/upload-base64 +Upload an image (base64-encoded) and get a stable public URL to reference +in a document via . + +Accepts PNG/JPEG/WebP/GIF/SVG, up to 10 MB decoded. Send raw base64 or a +data: URL in image_base64. This is the agent-friendly counterpart to the +browser multipart upload GÇö use it to save a generated or fetched image so a +document can embed it. + + + +# Upload a reference file (PDF/DOCX/image) for the AI to query while editing. +Source: https://docs.superdocs.app/api-reference/v1/upload-a-reference-file-pdfdocximage-for-the-ai-to-query-while-editing + +/openapi.json post /v1/attachments/upload-base64 +Files are processed asynchronously and become AI-searchable once ready. The AI can then reference the attachment's content during chat (e.g., "rewrite section 3 to match the style guide PDF I attached"). Images are queryable via multimodal vision. Poll get_attachment_status to know when ready. Distinct from upload_document_base64: attachments are read-only context for the AI to reference, not the editable working document. Supported formats: .pdf, .docx, .txt, .rtf, .md, .html, .htm. Max 50 MB validated; note the hosted transport layer caps any REQUEST BODY at ~32 MB and rejects larger ones at the gateway with an HTML (not JSON) error GÇö base64 inflates content ~33%, so inline uploads are reliable up to ~20 MB of file bytes; beyond that use request_upload_url (pre-signed flow, up to 100 MB). Requires session_id. + + + +# Upload an image and get back a stable URL the editor can drop into . +Source: https://docs.superdocs.app/api-reference/v1/upload-an-image-and-get-back-a-stable-url-the-editor-can-drop-into- + +/openapi.json post /v1/documents/images/upload +Upload a single inline image and return a stable URL you can reference +in the document via . + +Accepts PNG/JPEG/WebP/GIF/SVG, up to 10 MB per upload. The returned URL is +public-read with an unguessable path. Useful for saving a drawing or +screenshot so a document can embed it. + + + +# Upload Attachment +Source: https://docs.superdocs.app/api-reference/v1/upload-attachment + +/openapi.json post /v1/attachments/upload +Upload a document attachment to a session for AI reference. + +The file is processed asynchronously GÇö text is extracted, converted, and indexed +so the AI can search and reference it during chat. Use GET /v1/attachments/status/{session_id} +to check processing progress. + +Supported file types: .pdf, .docx, .txt, .rtf, .md, .html, .htm (max 50 MB). +Returns a job_id for tracking the processing status. + + + +# Upload Document To Editor +Source: https://docs.superdocs.app/api-reference/v1/upload-document-to-editor + +/openapi.json post /v1/documents/upload +Upload a document file and load it into the editor. + +Converts the file to HTML with formatting preserved (tables, colors, images). +Images are extracted to cloud storage and referenced by URL. +Returns the HTML for the editor to display. + +AI indexing happens automatically on the first chat message. +For API clients who want immediate indexing, pass ?index=true. + + + +# Upload .docx/PDF/HTML/MD/RTF as the active editable document with chunk-ID structural editing. +Source: https://docs.superdocs.app/api-reference/v1/upload-docxpdfhtmlmdrtf-as-the-active-editable-document-with-chunk-id-structural-editing + +/openapi.json post /v1/documents/upload-base64 +Parses the file into structured HTML where every paragraph, heading, table, row, and cell has a unique chunk ID, enabling the AI to make targeted structural edits via chat ("remove row 3 of the pricing table" works). Tables, borders, shading, alternating row colors, fonts, and inline styling are preserved on edit and export. Also works for AI-generated content: if you've drafted an outline or partial document in your context, upload it here as the working doc, then use chat to fill in the rest. This is also the reliable way to REPLACE a session's document with exact content you already hold GÇö the upload is a verbatim load, so it can never come back paraphrased or placeholder-shaped the way asking chat to re-type it can. When you pass a session_id the response is compact by default (metadata only GÇö pass return_html=true for the full parsed HTML); the document is loaded into the session and you edit it via chat. For files >100KB, prefer request_upload_url (pre-signed URL flow) to avoid token bloat from base64 through the agent context. Supported formats: .pdf, .docx, .txt, .rtf, .md, .html, .htm. Max 50 MB validated; note the hosted transport layer caps any REQUEST BODY at ~32 MB and rejects larger ones at the gateway with an HTML (not JSON) error GÇö base64 inflates content ~33%, so inline uploads are reliable up to ~20 MB of file bytes; beyond that use request_upload_url (pre-signed flow, up to 100 MB). + + + +# Upload User Template +Source: https://docs.superdocs.app/api-reference/v1/upload-user-template + +/openapi.json post /v1/templates/upload +Upload a document file as a personal/organization template. + +The file is processed synchronously: text extraction, HTML conversion, and content indexing. +Supported formats: .docx, .pdf, .txt, .rtf, .md, .html, .htm +Templates are scoped to the uploading user or organization. + + + +# Attachments +Source: https://docs.superdocs.app/concepts/attachments + +Upload PDF, DOCX, HTML, text, and image files to give the AI additional context or load them into the editor. + +# Attachments + +Upload files to provide the AI with reference material. The AI can search and reference attachment content when editing your document GÇö or you can load an attachment directly into the editor. + +## Supported file types + +| Type | Extensions | +| ------------ | ---------------------------------------- | +| PDF | `.pdf` | +| Word | `.docx`, `.doc` | +| OpenDocument | `.odt` | +| HTML | `.html`, `.htm` | +| Plain text | `.txt` | +| Rich text | `.rtf` | +| Markdown | `.md` | +| LaTeX | `.tex` (plus `.zip` project archives) | +| Images | `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp` | + +Legacy Word (`.doc`), OpenDocument (`.odt`), and Rich Text (`.rtf`) files are normalized into the same rich pipeline as `.docx`, so their headings, tables, and styling carry through. + +**LaTeX projects.** A `.tex` file uploads directly. A multi-file LaTeX project GÇö the shape editors export: a root `.tex` plus its `\input` chapters, figures, bibliography, and any custom class files GÇö uploads as a single `.zip`; the root document is detected automatically. Math imports losslessly (every equation keeps its LaTeX source and exports as native Word math), footnotes become real footnotes, bibliographies render with their citations, and source-declared page geometry, font size, and line spacing carry into the editor. Documents built on heavily visual custom classes (many r+¬sum+¬ and poster templates) are compiled and imported with their visual layout preserved. A `.zip` that contains no LaTeX root returns a structured error, and anything that can't be represented (for example an EPS figure or a TikZ drawing) is marked with a visible placeholder plus an ingest warning GÇö never silently dropped. Standard BibTeX (`.bib`) bibliographies resolve fully; biblatex-specific flows may degrade to bracketed citation keys plus an ingest warning. + +Maximum file size: up to **100 MB** through the pre-signed upload flow (`request_upload_url` GåÆ PUT GåÆ process) GÇö the path the web app and SDKs use automatically for anything large. Direct inline and multipart uploads are reliable up to about **20 MB**; an API-gateway request-body limit caps direct uploads below the pre-signed ceiling, so send larger files through the pre-signed flow (for files over \~100 KB it is preferable anyway, since bytes never pass through your context window). Oversized or unsupported uploads return a structured error naming the supported formats and the applicable limit. + +**Document scale.** A single document can expand into up to **25,000 editable sections** (roughly 6,000 pages), and one chat session can hold up to **30,000 sections** across all of its open documents (roughly 7,500 pages). These are stability limits for the hosted service, not platform caps: there is no hard platform limit on document length or on how many documents you work with in parallel. Crossing a limit returns a structured `413` (`DOCUMENT_TOO_COMPLEX` for a single file, `SESSION_TOO_FULL` for a chat) with `limit_expandable: true` and a contact address; the team is notified automatically and limits are raised on request, typically within a day, via [hello@superdocs.app](mailto:hello@superdocs.app). Larger dedicated deployments are available on Pro and Enterprise. + +## Upload a file + +Attachments are processed asynchronously. Upload the file, get a `job_id`, then poll for completion. The `job_id` is an opaque UUID GÇö attachment jobs are queried through the same `GET /v1/jobs/{job_id}` path as chat jobs (there's no separate id namespace or endpoint for them). + +```bash theme={null} +# 1. Upload +curl -X POST https://api.superdocs.app/v1/attachments/upload \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -F "file=@reference-doc.pdf" \ + -F "session_id=my-session" +``` + +Response: + +```json theme={null} +{ + "job_id": "550e8400-e29b-41d4-a716-446655440000", + "filename": "reference-doc.pdf", + "status": "processing", + "message": "Upload successful. Processing reference-doc.pdf..." +} +``` + +```bash theme={null} +# 2. Poll for completion +curl https://api.superdocs.app/v1/jobs/550e8400-e29b-41d4-a716-446655440000 \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +When `status` is `"completed"`, the attachment is indexed and available as AI context for that session. + +## Check attachment status + +```bash theme={null} +curl https://api.superdocs.app/v1/attachments/status/my-session \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +## Attachments vs document upload + +SuperDocs has two ways to load files GÇö they serve different purposes: + +| | Attachment (`/v1/attachments/upload`) | Document upload (`/v1/documents/upload`) | +| ------------------ | ----------------------------------------------------------------------------- | ---------------------------------------- | +| **Purpose** | Reference material for the AI | Load as the active document | +| **Processing** | Async (poll for completion) | Synchronous (immediate) | +| **Editing** | Read-only GÇö AI can search and reference | Fully editable via chat | +| **Multiple files** | Yes GÇö multiple per session | One active document per session | +| **Best for** | Supporting context (e.g., upload a brief, then create a document based on it) | Loading a file you want to edit | + +## Loading an attachment into the editor + +After uploading an attachment, you can ask the AI to load it into the editor GÇö making it the active document you can edit: + +```python theme={null} +# 1. Upload a file as an attachment +upload = requests.post("https://api.superdocs.app/v1/attachments/upload", + headers={"Authorization": f"Bearer {API_KEY}"}, + files={"file": open("draft.docx", "rb")}, + data={"session_id": "my-session"} +) + +# 2. Wait for processing to complete +# ... poll /v1/jobs/{job_id} until status is "completed" ... + +# 3. Ask the AI to load it into the editor +response = requests.post("https://api.superdocs.app/v1/chat", headers=HEADERS, json={ + "message": "Load the draft document into the editor", + "session_id": "my-session" +}) +# The AI loads the attachment content as the active document +``` + +The AI supports two loading modes: + +* **Replace** GÇö clears the editor and loads the full attachment (default when the editor is empty) +* **Insert** GÇö adds the attachment content to the existing document (when the editor already has content) + +## Image attachments and vision + +SuperDocs can **see** the images you attach. The AI visually interprets diagrams, screenshots, photos, charts, scanned documents, and any other image content GÇö and uses what it sees to inform the document it is editing. + +Common use cases: + +* **Transcribe a screenshot** into the document ("Add the table from this screenshot to my report") +* **Reference a diagram** in generated content ("Write a paragraph describing the architecture in this image") +* **Extract data from a chart** ("Summarize the trends in this chart and add a section about them") +* **Identify objects or text in a photo** ("What does the sign in this photo say? Add it as a quote") +* **Swap an image in the document** ("Replace the cover photo with this attached image", "Use this as the new company logo") GÇö pure URL swap, no AI generation cost + +The same `image_attachments` field also drives image generation and editing inside the document GÇö see [Visual content](/features#visual-content--media). Tell the AI **"generate an image of a glass office building at sunrise"** and it creates one and inserts it; click any image in the editor and tell the AI **"brighten this dramatically"** or **"recolor with warm sepia"** and the AI rewrites the bytes. + +Images are handled inline with your chat request GÇö no separate upload step is required: + +```json theme={null} +{ + "message": "What does this diagram show? Add a section explaining it.", + "session_id": "my-session", + "image_attachments": [{ + "id": "img-1", + "name": "diagram.png", + "base64Data": "iVBORw0KGgo...", + "mimeType": "image/png", + "size": 54321 + }] +} +``` + +Supported image formats: PNG, JPG/JPEG, GIF, WebP. + +## Delete an attachment + +```bash theme={null} +curl -X DELETE "https://api.superdocs.app/v1/attachments/ATTACHMENT_ID?session_id=my-session" \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + + +# Cross-session memory & search +Source: https://docs.superdocs.app/concepts/cross-session-context + +Let the AI remember your preferences across chats and reuse your own prior documents GÇö opt-in, off by default, and scoped to you. + +# Cross-session memory & search + +By default, each [session](/concepts/sessions) is self-contained: the AI only knows about the conversation and documents in that session. Two **opt-in** capabilities let a session reach beyond itself: + +* **Cross-session memory** GÇö the AI remembers your durable preferences and recurring instructions across chats (e.g. "always use British spelling", "our company name is Acme Ltd"), so you don't repeat them every time. +* **Cross-session search** GÇö the AI can find and reuse your own prior documents and chats ("pull the payment terms from last month's contract") instead of being limited to the current session. + +Both are **off by default**. You turn them on per request with the `cross_session_*` fields on `chat` / `chat_async` (REST and MCP). Both are **owner-scoped** GÇö they never reach across the API key owner. One customer's data is never visible to another. + +## Turning it on + +Pass the flags on a chat request. Omit them (or set them to `false`) and behavior is exactly as it is today GÇö single-session only. + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/chat \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "message": "Draft a follow-up using the same tone as my previous proposals", + "session_id": "new-proposal", + "cross_session_memory": true, + "cross_session_search": true + }' +``` + +| Field | Type | Default | What it does | +| -------------------------- | ------------------- | ------- | ------------------------------------------------------------------------------------------------------------ | +| `cross_session_memory` | boolean | `false` | The AI applies durable preferences it has learned from your past chats, and may note new ones for next time. | +| `cross_session_search` | boolean | `false` | The AI can find and reuse content from your own prior documents and chats. | +| `cross_session_memory_key` | string | GÇö | Keeps a **separate** memory per end-customer (B2B2C GÇö see below). | +| `cross_session_scope` | list of session ids | GÇö | Narrows cross-session search to a specific set of sessions instead of everything you own. | + +## What memory is (and isn't) + +Memory is about **how you like to work**, not a transcript of everything you've written. Think of it as the assistant remembering your standing instructions: preferred tone, spelling conventions, recurring names and facts, formatting habits. It's there so the AI carries your preferences from one chat to the next without you restating them. + +Memory is **not** a dump of your document content into every prompt. Cross-session search is the capability that pulls in actual prior content, and only when a request calls for it. + +## Owner scoping + +Everything stays inside the API key owner's data. With cross-session search on, the AI can reach your other sessions and documents GÇö and only yours. There is no path for it to surface another account's or another organization's content. Memory follows the same boundary. + +## B2B2C: separate memory and scope per end-customer + +If you serve multiple end-users through a single API key, you don't want one user's preferences leaking into another user's chats. Two controls handle this: + +* **`cross_session_memory_key`** GÇö pass a stable per-end-customer key (e.g. your own user id) and the AI keeps a **distinct memory** for each one. User A's preferences never affect User B's drafts. +* **`cross_session_scope`** GÇö pass the list of session ids that belong to that end-customer so cross-session search only looks at their work, not the whole account's. + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/chat \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "message": "Continue this in the usual style", + "session_id": "user_123_draft-7", + "cross_session_memory": true, + "cross_session_memory_key": "user_123", + "cross_session_search": true, + "cross_session_scope": ["user_123_draft-1", "user_123_draft-2"] + }' +``` + +Use `cross_session_memory_key` and `cross_session_scope` together to give each of your end-customers their own private memory and their own search boundary while still running under one API key. + +## Clearing memory + +To wipe the durable memory the AI has built up, clear it explicitly. This removes the stored preferences GÇö it does not touch your documents or chat history. + +```bash theme={null} +curl -X DELETE https://api.superdocs.app/v1/users/me/cross-session-memory \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +For a B2B2C setup, pass the same value you've been sending as `cross_session_memory_key` as the `memory_key` **query parameter**, so you clear only that end-customer's memory (omitting it clears the account-level note): + +```bash theme={null} +curl -X DELETE 'https://api.superdocs.app/v1/users/me/cross-session-memory?memory_key=user_123' \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +The MCP tool is `clear_cross_session_memory`. + + + **`clear_cross_session_memory` is destructive.** It permanently erases the stored memory note (scoped to the `memory_key` you pass, or your account-wide memory if you pass none). The preferences are gone after this call. If you expose it to an AI agent, confirm intent before calling it. + + +## Notes + +* Both capabilities are strictly opt-in per request. A request that doesn't set the flags behaves exactly like a single-session chat. +* Memory captures preferences over time; you may not see its effect on the very first chat after enabling it. +* Memory and search are independent GÇö you can enable one without the other. +* See [Multi-Document Sessions](/guides/multi-document) for working with several documents inside a single session (a separate concept from reaching across sessions). + + +# Documents +Source: https://docs.superdocs.app/concepts/documents + +Load documents from files, edit them with AI, create new ones from scratch, and export as Word files. + +# Documents + +## Sending document HTML + +Send your document HTML in `document_html`. The AI reads it, makes changes, and returns updated HTML in `document_changes.updated_html`. + +**The server keeps your document between turns.** Once a session holds a document, you don't re-send it every turn. Send just the `message` and the AI works on the copy it already has. Include `document_html` only to **load** a document into a session the first time, **replace** it wholesale, or **sync** edits you made to the HTML outside of chat (covered below). + +**The one rule, for when you *do* send HTML back: send it faithfully.** If the user edited the document in your own editor and you're syncing those edits, send the full current HTML exactly as your editor has it. Don't programmatically strip, modify, or reformat it. If your editor or HTML sanitizer removes custom `data-*` attributes, configure it to preserve them. Those `data-chunk-id` attributes are what let the AI make targeted edits. + +### Example flow + +```python theme={null} +import requests + +API_KEY = "sk_YOUR_API_KEY" +HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"} + +# 1. Send a document +response = requests.post("https://api.superdocs.app/v1/chat", headers=HEADERS, json={ + "message": "Add a summary at the top", + "session_id": "my-doc", + "document_html": "

Report

Q4 revenue increased by 15%.

" +}) + +data = response.json() +updated_html = data["document_changes"]["updated_html"] + +# 2. Follow-up turn: the server still holds the document, so send just the message +response = requests.post("https://api.superdocs.app/v1/chat", headers=HEADERS, json={ + "message": "Make the summary more concise", + "session_id": "my-doc", +}) + +# Re-send document_html ONLY when you changed the HTML outside chat +# (e.g. the user edited it in your editor) and want the AI to see those edits: +# +# response = requests.post("https://api.superdocs.app/v1/chat", headers=HEADERS, json={ +# "message": "Make the summary more concise", +# "session_id": "my-doc", +# "document_html": edited_html, # send it exactly as your editor has it +# }) +``` + +The returned HTML contains `data-chunk-id` attributes on elements. These identify document sections and enable the AI to make targeted edits without reprocessing the entire document. These same IDs appear in API responses GÇö for example, `chunk_id` and `insert_after_chunk_id` in [HITL proposed changes](/guides/human-in-the-loop#understanding-proposed-changes). + +If you make an edit you want to undo, you can rewind both the chat history and the document state to before any user message GÇö see [Revert a session to a previous message](/concepts/sessions#revert-a-session-to-a-previous-message). + +## Loading documents from files + +Upload a file to load it as the active document in a session. The file is processed synchronously GÇö the response includes the full document HTML ready for editing. + +**Supported formats:** DOCX, DOC, ODT, PDF, TXT, HTML, MD (Markdown), RTF + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/documents/upload \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -F "file=@contract.docx" \ + -F "session_id=my-session" +``` + +Response: + +```json theme={null} +{ + "html": "

Contract

...

", + "session_id": "my-session", + "filename": "contract.docx", + "chunks_count": 12, + "version_id": "v_xyz789" +} +``` + +**Multiple documents per session.** By default an upload **replaces** the session's focused document. Pass `open_mode=new_focused` to open the file as an additional document (and focus it), or `open_mode=background` to open it without stealing focus. The response also includes the session's document roster (every open document's id + title + which is focused) so you can render tabs immediately. See the [Multi-Document Sessions guide](/guides/multi-document). + +**Page geometry.** For DOCX and PDF uploads, document payloads include a nullable `page_setup` object GÇö detected page dimensions (`width_in` / `height_in`), per-side margins (`margin_in`), and `orientation` GÇö anywhere a document is returned (upload responses, the session-documents roster, session history). It's `null` when the source format carries no geometry (e.g. plain text or HTML). + +```json theme={null} +{ + "page_setup": { + "width_in": 8.27, + "height_in": 11.69, + "margin_in": {"top": 1.0, "right": 1.0, "bottom": 1.0, "left": 1.0}, + "orientation": "portrait", + "source": "docx" + } +} +``` + +After uploading, you can immediately send chat messages to edit the document: + +```python theme={null} +# Upload a document +upload = requests.post("https://api.superdocs.app/v1/documents/upload", + headers={"Authorization": f"Bearer {API_KEY}"}, + files={"file": open("contract.docx", "rb")}, + data={"session_id": "my-session"} +) +doc_html = upload.json()["html"] + +# Edit it with AI +response = requests.post("https://api.superdocs.app/v1/chat", headers=HEADERS, json={ + "message": "Simplify the language in section 3", + "session_id": "my-session", + "document_html": doc_html +}) +``` + +## Creating documents from scratch + +Send a message to an empty session (no `document_html`, no uploaded file) and the AI will generate a complete document for you. + +```python theme={null} +response = requests.post("https://api.superdocs.app/v1/chat", headers=HEADERS, json={ + "message": "Create a non-disclosure agreement between two companies", + "session_id": "new-nda" +}) + +data = response.json() +new_document_html = data["document_changes"]["updated_html"] +``` + +Example prompts that work well: + +* "Create a consulting agreement" +* "Draft a project proposal for a mobile app" +* "Write a company privacy policy" +* "Create a meeting minutes template" + +The AI generates structured, formatted HTML that you can then continue editing with follow-up messages. + +## Exporting documents + +Export the current document as a downloadable file. + +**Three ways to supply the document:** + +* **Inline HTML** GÇö send HTML content in `html` (use when you have the document HTML in your app, up to 20 MB) +* **Export from a session** GÇö send a `session_id` (the API retrieves the document from the session) +* **Pre-signed upload then export** GÇö upload the HTML to a pre-signed URL first, then send `upload_id` (use for documents between 20 MB and 100 MB GÇö see [Large documents](#large-documents) below) + +**Five output formats:** + +* `docx` (default) GÇö Microsoft Word (Open XML), preserves tables, formatting, embedded images +* `pdf` GÇö paginated, print-ready PDF +* `html` GÇö standalone HTML file with inlined CSS +* `markdown` GÇö Markdown (.md) with ATX headings +* `txt` GÇö plain text + +A sixth value, `doc` (Word-compatible HTML wrapper), is accepted as a legacy alias and will be removed in a future release. New integrations should use `docx`. + +### Export from inline HTML + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/documents/export \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "html": "

My Document

Content here...

", + "format": "docx", + "options": { "filename": "my-document" } + }' \ + --output my-document.docx +``` + +### Export from a session + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/documents/export \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "session_id": "my-session", + "format": "pdf" + }' \ + --output exported-document.pdf +``` + +### Export in Python + +```python theme={null} +response = requests.post("https://api.superdocs.app/v1/documents/export", + headers=HEADERS, + json={ + "session_id": "my-session", + "format": "docx", + "options": { + "paper_size": "A4", + "margins": "normal", + "filename": "final-contract" + } + } +) + +with open("final-contract.docx", "wb") as f: + f.write(response.content) +``` + +### Request parameters + +| Parameter | Type | Required | Description | +| ------------ | ------ | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| `html` | string | One of `html` / `session_id` / `upload_id` required | HTML content to convert (inline HTML) | +| `session_id` | string | One of `html` / `session_id` / `upload_id` required | Session to export from | +| `upload_id` | string | One of `html` / `session_id` / `upload_id` required | Pre-signed upload reference (for large documents) | +| `format` | string | No | `"docx"` (default), `"pdf"`, `"html"`, `"markdown"`, or `"txt"`. `"doc"` accepted as a legacy alias. | +| `options` | object | No | Customisation block GÇö see [Export options](#export-options) below. | +| `filename` | string | No | Legacy top-level filename. Prefer `options.filename`, which wins when both are set. | + +### Export options + +Pass an `options` object to customise the rendered output. Defaults are sensible for English-language documents; override only what you need. + +| Field | Type | Default | Description | +| ----------------------- | ------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `paper_size` | string | `"Letter"` | `"Letter"`, `"A4"`, `"A3"`, or `"Legal"`. Applies to DOCX/PDF. | +| `orientation` | string | `"portrait"` | `"portrait"` or `"landscape"`. Applies to DOCX/PDF. | +| `margins` | string | `"normal"` | `"narrow"` (0.5 in), `"normal"` (1 in), `"wide"` (1.5 in), or `"custom"`. | +| `custom_margins_inches` | object | GÇö | Required when `margins="custom"`. Object with `top`, `right`, `bottom`, `left` floats between 0.25 and 3.0. | +| `filename` | string | GÇö | Name for the downloaded file. Precedence: `options.filename`, then the legacy top-level `filename`, then a name derived from the document's first heading. Pass it with or without an extension: a supplied extension matching the export format is kept as-is (never doubled), and an extension-less name gets the format's extension appended. | +| `embed_images` | boolean | `false` | HTML export only. When `true`, images are base64-embedded for offline portability. Raises the size cap from 100 MB to 150 MB. | +| `watermark_text` | string | GÇö | PDF only. Optional text watermark overlaid on every page (max 64 chars). | +| `watermark_opacity` | number | `0.3` | PDF watermark opacity, between 0.05 and 1.0. | + +```python theme={null} +options = { + "paper_size": "A4", + "orientation": "landscape", + "margins": "wide", + "filename": "Q4-Report", + "watermark_text": "DRAFT", + "watermark_opacity": 0.15, +} +``` + +The response is a binary file download with the appropriate `Content-Type` header. The `Content-Disposition` header carries the filename (RFC 5987 encoded for non-ASCII characters). + +### Non-fatal warnings + +Exports may complete successfully but with non-fatal issues GÇö an image URL that 404'd, a diagram that exceeded the render timeout, an unsupported field code that was skipped. The response carries these in the `X-Export-Warnings` header as a base64-encoded JSON list. The header is always present alongside `Content-Disposition` in `Access-Control-Expose-Headers`, so browser clients can read it via `fetch`. + +```javascript theme={null} +const response = await fetch('/v1/documents/export', { ... }); +const warningsHeader = response.headers.get('X-Export-Warnings'); +if (warningsHeader) { + const warnings = JSON.parse(atob(warningsHeader)); + // [{ code: "image_download_failed", message: "...", detail: { src: "..." } }, ...] +} +``` + +| Warning code | Meaning | +| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `image_download_failed` | An embedded image URL could not be fetched. The image is omitted from the output. | +| `mermaid_render_failed` | A Mermaid diagram could not be rendered. The diagram source is kept in the output as a code block. | +| `mermaid_timeout` | A Mermaid diagram took too long to render. Same fallback as above. | +| `excalidraw_corrupt` | An Excalidraw drawing's JSON payload could not be parsed. | +| `footnote_orphan` | A footnote reference has no matching footnote body. | +| `track_change_unmatched` | A tracked change anchor could not be placed inline; the change is appended at the end of the document. | +| `field_code_unsupported` | A citation or other field code could not be re-emitted in the output format. | +| `watermark_skipped` | A requested PDF watermark could not be applied. | +| `embed_image_skipped` | An image could not be inlined into an HTML export with `embed_images=true`. | +| `size_cap_warning` | The document is close to the per-format size limit. | +| `header_footer_degraded` | A rich page header or footer could not be fully reproduced; it was emitted as best-effort text with working page-number fields. | +| `sections_degraded` | Per-section page geometry (a section with its own page size or orientation) could not be applied in this PDF render; the export uses a single page setup. | +| `svg_png_conversion_failed` | An SVG image could not be converted for `.docx` output; the original SVG is kept in the file. | +| `math_partial_fidelity` | An equation contained a construct that could not be fully converted to native Word math; that construct is kept as its LaTeX source inside the equation. | + +### Large documents + +There's one size story for export, driven by how big the document HTML is: + +**Three-tier flow:** + +| Document HTML size | Path | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Up to 20 MB | Direct POST to `/v1/documents/export` with inline `html` or a `session_id`. | +| 20 MB GÇô 100 MB | Request a pre-signed upload URL via `POST /v1/uploads` (`purpose: "export-html"`), PUT the HTML to it, then call `/v1/documents/export` with `upload_id`. | +| Over 100 MB | Use the [email-fallback endpoint](#email-fallback-for-very-large-documents). | + +Keep inline POSTs at or under 20 MB. Above that, the hosted transport layer rejects the request (a hard \~32 MB clamp sits in front of the renderer), so switch to the upload-then-export pattern (PUT the HTML to a pre-signed URL, then export by `upload_id`) rather than pushing a bigger body through directly. + +```python theme={null} +# 1. Request a pre-signed PUT URL +upload = requests.post("https://api.superdocs.app/v1/uploads", + headers=HEADERS, + json={ + "filename": "export.html", + "content_type": "text/html", + "size_bytes": len(html.encode()), + "purpose": "export-html", + } +).json() + +# 2. PUT the HTML directly to storage +requests.put(upload["upload_url"], + headers={"Content-Type": "text/html"}, + data=html) + +# 3. Export, referencing the upload_id +response = requests.post("https://api.superdocs.app/v1/documents/export", + headers=HEADERS, + json={ + "upload_id": upload["upload_id"], + "filename": "export.html", + "format": "docx", + } +) + +with open("output.docx", "wb") as f: + f.write(response.content) +``` + +The signed URL is valid for 5 minutes; the uploaded blob is retained for 24 hours. + +### Email fallback for very large documents + +For documents over 100 MB, render asynchronously and deliver via email. The endpoint accepts a session ID, queues a background job, and emails a 7-day signed download link to the recipient. + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/documents/export/email-request \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "session_id": "my-session", + "format": "docx", + "recipient_email": "user@example.com", + "options": { "paper_size": "A4" } + }' +``` + +Response: + +```json theme={null} +{ + "job_id": "9c2f...e3a", + "recipient_email": "user@example.com", + "status": "queued", + "eta": "24h", + "message": "Your file will be emailed to user@example.com within 24 hours." +} +``` + +`recipient_email` falls back to the email on file for the authenticated user account when omitted. The job appears in `/v1/jobs/{job_id}` with `job_type: "large_export"`. See [Async jobs](/guides/async-jobs) for the polling pattern. + +### Size and error responses + +| Format | Cap | +| ------------------------------- | ------ | +| `docx`, `pdf`, `html`, `doc` | 100 MB | +| `html` with `embed_images=true` | 150 MB | +| `markdown`, `txt` | 50 MB | + +Keep inline POSTs at or under 20 MB; the hosted transport layer enforces a hard \~32 MB clamp and returns `413 Request Entity Too Large` before reaching the renderer, so use the pre-signed upload-then-export pattern for anything larger. Requests above the format-specific cap return a structured 413 with a JSON body explaining the limit and pointing to the email-fallback endpoint GÇö see [Error codes](/errors/error-codes#413-request-entity-too-large). + +## Supported formatting + +The AI can apply the following formatting when creating or editing documents: + +* **Text styling** GÇö bold, italic, underline, strikethrough +* **Headings** GÇö H1 through H6 +* **Lists** GÇö ordered, unordered, and nested +* **Text highlighting** GÇö with color options (e.g., yellow, green, red) +* **Text color** GÇö change the color of specific text +* **Links** GÇö clickable hyperlinks +* **Tables** GÇö with rows and columns +* **Blockquotes** GÇö indented quotation blocks +* **Code blocks** GÇö for code snippets +* **Horizontal rules** GÇö section dividers + +Ask for formatting in plain language: "highlight the key terms in yellow", "make the title bold and larger", "add a table comparing the two options". + + +# Files (durable documents) +Source: https://docs.superdocs.app/concepts/files + +Documents persist across sessions, so you can list, open, rename, and archive them GÇö and reopen a saved document in any new session. + +# Files (durable documents) + +Every document you create or upload is saved as a durable **File**. Files outlive the session they were made in: you can list them later, reopen them in a brand-new session, rename them, and archive ones you no longer need. A File is a reusable document, not a copy that disappears when a conversation ends. + +This is the layer above [sessions](/concepts/sessions). A session is a conversation thread; a File is the document itself. The same File can be opened into different sessions over time. + +Everything here works identically over the REST API and the MCP tools. + +## The mental model + +* **File** GÇö a durable document with a stable id and a title. It persists across sessions until you archive it. +* **Session** GÇö a conversation thread. Opening a File into a session lets you edit it there; closing it from the session leaves the File untouched. +* **Open** GÇö attaching a saved File to a session so you can work on it. Open is **share-attach, not copy**: edits flow back to the one underlying File, so reopening it elsewhere shows your latest changes. + +## List your Files + +```bash theme={null} +curl https://api.superdocs.app/v1/documents \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +Returns your saved documents with their ids, titles, and timestamps. Use the title for display labels. The list is token-light: it never returns a full document body. Pass `include_preview=true` to also get a small `preview_html` thumbnail (the first few sections) per document, and page through results with `limit` and `offset`. To read one document's full body, use `get_document_detail` with `include_html=true` (below) or open it into a session. + +The MCP tool is `list_documents`. + +## Get one File's detail + +```bash theme={null} +curl https://api.superdocs.app/v1/documents/{document_id} \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +Returns the File's title, metadata, and the list of prior chats that used it. Add `?include_html=true` to also get the full document `html` body back, along with its `page_setup` and `version_id`. This is how you read a saved File's content by id without opening it into a session. + +The MCP tool is `get_document_detail`. + +## Open a saved File into a session + +To keep working on a File, open it into a session. You can open one or more Files at once; the first one you list becomes the [focused tab](/guides/multi-document). + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/sessions/my-session/documents/open \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "document_ids": ["doc_2f6cGǪ", "doc_9a1bGǪ"] + }' +``` + +The response returns the session's document roster (every open document's id + title + which is focused) so you can render tabs immediately. Because open is share-attach, edits you make in the session update the underlying File GÇö there's no separate copy to reconcile. + +The MCP tool is `open_documents`. + +### Start a session and open Files in one call + +If you're beginning fresh, `init_session` creates a session and opens one or more saved Files into it in a single request GÇö handy for "reopen my last contract and start editing." + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/sessions/init \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "session_id": "contract-review", + "document_ids": ["doc_2f6cGǪ"] + }' +``` + +The MCP tool is `init_session`. + +## Rename a File + +```bash theme={null} +curl -X PATCH https://api.superdocs.app/v1/documents/{document_id} \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"title": "Q3 Vendor Agreement"}' +``` + +The new title is what shows up in your File list and on tabs. + +The MCP tool is `rename_document`. + +## Archive a File + +Archiving removes a File from your active list. It's a **soft-delete: recoverable**, not an immediate permanent erase. Archived Files stay restorable for roughly 30 days before they are purged, after which they're gone for good. + +```bash theme={null} +curl -X DELETE https://api.superdocs.app/v1/documents/{document_id} \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +Two optional query params control what happens when the File is currently open somewhere: + +* `?force=true` GÇö archive even if the File is still open in a live session. Without it, archiving a File that another session is actively holding fails with **409 `document_in_use`** (`open_in_sessions` + a suggested recovery) and leaves it untouched, so you don't pull a document out from under someone mid-edit GÇö and you always know nothing was archived. +* `?from_session={session_id}` GÇö also detach the File from that session as part of the archive. + +The MCP tool is `archive_document`. + + + **`archive_document` is destructive.** It's a soft-delete GÇö the File is recoverable for about 30 days and then permanently purged. If you expose this to an AI agent, treat it like any other delete: confirm intent before calling it. There is no separate permanent-delete-by-default; archiving is the only removal path, and the \~30-day window is your safety net. + + +## Restore (un-archive) a File + +Bring an archived File back to your active list GÇö the clean inverse of archive. It takes the same durable `document_id` you'd pass to archive, so you can round-trip archive Gåö restore by the same id (no session needed). + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/documents/{document_id}/unarchive \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +Idempotent GÇö restoring an already-active File is a no-op. The MCP tool is `unarchive_document`. (Both are non-billable.) + +## Files vs sessions vs open documents + +| | File | Session | Open document | +| -------------- | --------------------------------------------- | ------------------------------------------- | --------------------------------------------------------------- | +| **What it is** | A durable, reusable document | A conversation thread | A File attached to a session for editing | +| **Lifetime** | Persists until archived (then \~30-day purge) | Persists across restarts; you choose the id | Lasts while open in the session | +| **Closing it** | n/a | Delete the session | Removes it from the session only GÇö the File is untouched | +| **Editing** | Edited via whatever session it's open in | Holds the chat history | Edits flow back to the underlying File (share-attach, not copy) | + +## Notes + +* Closing a document from a session (see [Multi-Document Sessions](/guides/multi-document)) does **not** archive or delete the File GÇö it only detaches it from that session. +* Because open is share-attach, the same File opened in two sessions reflects the same content; there is no fork-on-open. To rewind a specific session's edits, use [revert](/concepts/sessions#revert-a-session-to-a-previous-message). +* Files are scoped to the API key owner GÇö your saved documents are only visible to you (or your organization). + + +# Sessions +Source: https://docs.superdocs.app/concepts/sessions + +Sessions persist conversation history and document state across requests. You choose the session ID. + +# Sessions + +A session holds your conversation history and document state. Reuse a session ID to pick up where you left off. + +## How sessions work + +You choose the `session_id` GÇö any string you want. The API stores the conversation and document state for that session. + +```bash theme={null} +# First request GÇö starts a new session +curl -X POST https://api.superdocs.app/v1/chat \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "message": "Add an introduction section", + "session_id": "project-proposal-v1", + "document_html": "

Project Proposal

" + }' + +# Second request GÇö continues the same session +curl -X POST https://api.superdocs.app/v1/chat \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "message": "Now add a budget section", + "session_id": "project-proposal-v1", + "document_html": "
...updated HTML from previous response...
" + }' +``` + +The AI remembers the full conversation. On the second request, it knows about the introduction it just wrote. + + + You don't have to re-send `document_html` on every turn. Once the session holds a document the server keeps it, so a follow-up can send just the `message` (the example above re-sends it to also sync any edits the client made). Re-send the HTML only to replace the document or to sync edits you made outside chat. See [Documents](/concepts/documents#sending-document-html). + + +A session can also hold **multiple open documents** at once GÇö each with its own `document_id`, with one document focused at a time. Chat turns target the focused document unless you pass `document_id` (or name a document in the message). See the [Multi-Document Sessions guide](/guides/multi-document). + +## Persistence + +Sessions persist across server restarts. You can close your browser, come back tomorrow, and resume with the same `session_id`. + +**Sessions do not expire.** There is no retention window or time-based cleanup: a session from six months ago is just as resumable as one from this morning. Conversation history and document state remain available until you delete the session yourself (see [Delete a session](#delete-a-session) below). Note that archived durable Files follow their own lifecycle and are purged about 30 days after archiving; see [Files](/concepts/files). + +## List sessions + +```bash theme={null} +curl https://api.superdocs.app/v1/sessions \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +Returns session IDs, message counts, timestamps, and a preview of the first message. + +## Load session history + +```bash theme={null} +curl https://api.superdocs.app/v1/sessions/project-proposal-v1/history \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +The response includes: + +* `messages` GÇö Full conversation history. Each message carries `turn_index` (the position in the session) and `checkpoint_id` (a marker used by the revert endpoint; `null` for older messages recorded before the revert feature shipped). +* `document_state` GÇö Current document HTML, version ID, and attachment list +* `editor_action` GÇö What to do with the document: `"update"` (load the HTML), `"clear"` (reset), or `"keep"` (no change) + +## Revert a session to a previous message + +Rewind both the chat and the document to the state immediately before a specific user message. + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/sessions/project-proposal-v1/revert \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"turn_index": 4}' +``` + +`turn_index` is the position of the **user message** you want to undo. Pass it from a `messages[i].turn_index` you got back from the history endpoint above. + +The response gives you: + +* `compose_text` GÇö The text of the user message that was reverted. Useful to pre-fill a compose box so the user can edit and resend. +* `reverted_to_turn` GÇö The turn the conversation now ends at (the previous AI reply), or `-1` if the session was reset to its initial empty state. +* `document_state` + `editor_action` GÇö Same shape as the history endpoint. Apply them to your editor. +* `archived_turn_count` GÇö How many rows were soft-archived. + +What happens: + +* The session is restored to the state at that point in the conversation GÇö both the chat history and the document are rewound to the previous AI reply. Messages and document state from the reverted point onward are retained for audit but hidden from active reads. +* Subsequent messages on this session continue from that restored point. There is no timeline-switcher in the API today GÇö the restored timeline becomes the only visible one. +* If the session has a chat job currently running (in progress or awaiting approval), the revert call returns `409`. Wait for the job to settle or cancel it first. +* If the user message predates the revert feature (its `checkpoint_id` is `null`), the call returns `422`. Older sessions can still be loaded; only their per-message revert is unavailable. + +## Session isolation + +Sessions are isolated per user. Your sessions are never visible to other users. + +## B2B session management + +If you're serving multiple end users through a single API key, manage session IDs per user on your end. For example, prefix session IDs with your user's identifier: + +``` +session_id: "user_123_draft-contract" +session_id: "user_456_meeting-notes" +``` + +This keeps each user's conversations and documents separate. + +## Delete a session + +Session deletion lives at `/v1/users/me/sessions/{id}`, which is part of the web-app account surface GÇö it accepts web-app session tokens only and **rejects `sk_` / `lce_` API keys with a `401`**. Delete sessions from the browser at [use.superdocs.app](https://use.superdocs.app) GåÆ Settings. + +From an API-key context you don't need to delete sessions: they persist server-side but are never billed unless used, so simply stop reusing a `session_id` to retire it. Each user's sessions stay isolated regardless. + + +# Templates +Source: https://docs.superdocs.app/concepts/templates + +Upload reusable document templates that the AI can search and apply when creating content. + +# Templates + +Upload document templates for the AI to reference. When you ask the AI to create a document, it can search your templates and use them as a starting point. + +## Upload a template + +Templates support the same file types as attachments: `.docx`, `.pdf`, `.txt`, `.rtf`, `.md`, `.html`, `.htm`. + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/templates/upload \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -F "file=@nda-template.docx" +``` + +Template processing is synchronous GÇö the template is ready immediately after upload. + +## List your templates + +```bash theme={null} +curl https://api.superdocs.app/v1/templates \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +## Delete a template + +```bash theme={null} +curl -X DELETE https://api.superdocs.app/v1/templates/TEMPLATE_ID \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +## How the AI uses templates + +When you ask the AI to create a document (e.g., "Draft an NDA"), it searches your uploaded templates for relevant matches. If a template fits, the AI uses it as a starting point and customizes it based on your instructions. + +Templates are scoped per user or organization GÇö your templates are only visible to you. + + +# Error Codes +Source: https://docs.superdocs.app/errors/error-codes + +HTTP status codes, error response format, and common error scenarios for the SuperDocs API. + +# Error Codes + +All API errors return a JSON response with a `detail` field: + +```json theme={null} +{ + "detail": "Human-readable error message" +} +``` + +## Status codes + +| Code | Meaning | Common causes | +| ------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **400** | Bad Request | Empty message, invalid parameters | +| **401** | Unauthorized | Missing or invalid API key, expired token | +| **403** | Forbidden | Accessing a resource you don't own; wrong agent-takeover code (the `detail` says how many attempts remain) | +| **404** | Not Found | Job, session, template, or attachment doesn't exist; revert target message not in active history; pre-signed upload expired; expired or unknown agent-takeover link | +| **409** | Conflict | Revert blocked because a chat job is currently running on the same session | +| **410** | Gone | Agent-takeover link permanently disabled after too many incorrect codes. Terminal: the old link can never work again; the agent must send a fresh handoff link | +| **413** | Request Entity Too Large | Request body or export payload above the size cap | +| **415** | Unsupported Media Type | Upload file extension not supported, including a legacy `.doc` upload (convert to `.docx`); use `.pdf`, `.docx`, `.txt`, `.rtf`, `.md`, `.html`, `.htm` | +| **422** | Validation Error | Request body doesn't match expected schema; format value outside the allowed enum; revert target message predates the revert feature | +| **429** | Too Many Requests | Monthly operation limit reached, or an endpoint-specific cooldown. Application 429s are JSON and always carry a `Retry-After` header (seconds); see [Rate Limits](/errors/rate-limits) | +| **500** | Internal Server Error | Unexpected server error | +| **504** | Gateway Timeout | AI processing exceeded 30 minutes | + +## Common errors and fixes + +### Authentication errors (401) + +```json theme={null} +{"detail": "Invalid authorization header format. Expected 'Bearer '"} +``` + +**Fix:** Use `Authorization: Bearer sk_YOUR_KEY` (include the `Bearer ` prefix). + +```json theme={null} +{"detail": "Invalid API key"} +``` + +**Fix:** Check that your key is correct and hasn't been revoked. Generate a new key from Settings > API Keys. + +### Validation errors (400) + +```json theme={null} +{"detail": "Message cannot be empty. Please provide a text message."} +``` + +**Fix:** Include a non-empty `message` in your request body. + +### Unsupported file type (415) + +```json theme={null} +{"detail": "Unsupported file type: .xyz. Supported types: .pdf, .docx, .txt, .rtf, .md, .html, .htm"} +``` + +A legacy binary `.doc` upload returns the same `415`: + +```json theme={null} +{"detail": "Unsupported file type: .doc. Supported types: .pdf, .docx, .txt, .rtf, .md, .html, .htm"} +``` + +**Fix:** Upload one of the supported formats GÇö `.pdf`, `.docx`, `.txt`, `.rtf`, `.md`, `.html`, `.htm`. Convert legacy `.doc` files to `.docx` first. RTF is supported as an upload **input** only; it is not an export target. Catch `415` (Unsupported Media Type) separately from `400` when handling upload-validation failures. + +### Rate limit (429) + +```json theme={null} +{"detail": "Your monthly operation quota (500) is exhausted. Upgrade to Pro for uninterrupted pay-as-you-go overage."} +``` + +**Fix:** Upgrade your plan at Settings > Billing, or wait for your monthly reset. Honor the `Retry-After` header (seconds until the request can succeed) and branch on the `429` status and the `X-Usage-Limit` / `X-Usage-Used` / `X-Usage-Remaining` headers rather than the `detail` string (the wording may change). Check the `usage` object in chat responses to monitor remaining operations. A plain-text `429` with no `Retry-After` is an infrastructure surge response, not an application error; back off with jitter and retry (see [Rate Limits](/errors/rate-limits)). + +### Timeout (504) + +Both the synchronous `/v1/chat` and the async `/v1/chat/async` endpoints apply a 30-minute wall-clock cap, but they surface the timeout differently: + +**Synchronous `/v1/chat`** returns an HTTP `504` with a plain `detail` string: + +```json theme={null} +{"detail": "The AI agent took longer than 30 minutes to process your request. For long edits or multi-step tasks, use chat_async."} +``` + +**Async `/v1/chat/async`** does not return a `504` on the poll GÇö the job completes and the friendly summary below is persisted as the job's AI response (read it from the job result): + +``` +GŦn+Å Your request ran past my 30-minute cap and I had to wrap up. For very large +documents, try splitting the task into multiple smaller turns GÇö e.g. one section +at a time. Your prompt is still in your history above; you can revise it and +re-send. +``` + +**Fix:** Use the async endpoint (`/v1/chat/async`) for long operations GÇö it streams `intermediate` progress events as the work runs and supports human-in-the-loop approval. Split large requests into smaller turns (one section per turn). + +### Request too large (413) + +The 413 response covers two distinct cases GÇö caller-too-large and document-too-large for the chosen export format GÇö with a structured JSON detail body so clients can react appropriately. + +**Caller payload exceeds the transport limit:** + +```json theme={null} +{ + "detail": "Request body too large", + "error_code": "request_too_large", + "message_user": "Your request is too large. The current limit is 100 MB.", + "request_size_mb": 142.3, + "max_size_mb": 100, + "suggested_action": "split_request", + "contact_email": "hello@superdocs.app", + "support_event_id": "abc1234567890" +} +``` + + + A much smaller \~32 MiB platform limit sits in front of the application. A body over that ceiling is rejected at the gateway with a generic error page (HTML), **not** this structured JSON body GÇö so don't try to parse it as JSON. Keep request bodies well under that limit; for large exports, switch to the upload-then-export pattern below. + + +**Fix:** For exports, switch to the upload-then-export pattern GÇö see [Large documents](/concepts/documents#large-documents). For other endpoints, split the work into smaller requests. + +**Export payload exceeds the format-specific cap:** + +```json theme={null} +{ + "detail": { + "error_code": "document_too_large", + "message_user": "This document is 142.3 MB, which exceeds the 100 MB export limit for the chosen format. Email hello@superdocs.app or use the in-app 'Email me a copy' button GÇö we'll send you the rendered file within 24 hours.", + "suggested_action": "email_request", + "contact_email": "hello@superdocs.app", + "document_size_mb": 142.3, + "max_size_mb": 100, + "format": "docx", + "support_event_id": "abc1234567890" + } +} +``` + +**Fix:** Call `POST /v1/documents/export/email-request` instead GÇö the file will be rendered in the background and emailed as a 7-day signed download link. See [Email fallback for very large documents](/concepts/documents#email-fallback-for-very-large-documents). + +| Field | Description | +| -------------------------------------- | -------------------------------------------------------------------------------- | +| `error_code` | `request_too_large` (transport layer) or `document_too_large` (export cap). | +| `message_user` | Human-readable explanation safe to surface in your UI. | +| `suggested_action` | `split_request` or `email_request`. Use to switch your client between paths. | +| `request_size_mb` / `document_size_mb` | Observed payload size. | +| `max_size_mb` | Configured cap for the format / endpoint. | +| `format` | Export format that triggered the cap (only on `document_too_large`). | +| `contact_email` | Support contact for follow-up. | +| `support_event_id` | Opaque correlation ID. Share with `hello@superdocs.app` when reporting an issue. | + +### Validation errors (422) + +```json theme={null} +{"detail": [{"type": "literal_error", "loc": ["body", "format"], "msg": "Input should be 'docx', 'pdf', 'html', 'markdown', 'txt' or 'doc'"}]} +``` + +**Fix:** `format` is now a strict enum on `/v1/documents/export`. Use one of `"docx"`, `"pdf"`, `"html"`, `"markdown"`, `"txt"`. The value `"doc"` is accepted as a legacy alias and will be removed in a future release. + +```json theme={null} +{"detail": [{"type": "string_pattern_mismatch", "loc": ["body", "session_id"], "msg": "String should match the expected pattern"}]} +``` + +**Fix:** `session_id` may contain only letters, digits, and the characters `_`, `-`, and `.`, up to a maximum of 256 characters. Drop spaces, slashes, and other punctuation from the id you choose. + +### Not found (404) + +```json theme={null} +{"detail": "Job not found"} +``` + +**Fix:** Jobs are cleaned up 1 hour after completion. Check the job ID and timing. Use `/v1/jobs` to list active jobs. + +### Revert conflicts (409) + +```json theme={null} +{"detail": "Cannot revert while a chat job is running for this session. Wait for it to complete or cancel it first."} +``` + +**Fix:** Wait for the in-flight job to finish (poll `/v1/jobs/{job_id}` or watch the SSE stream until you see `final`), or cancel it via `POST /v1/jobs/{job_id}/cancel` before retrying revert. + +### Revert target too old (422) + +```json theme={null} +{"detail": "This message predates the revertability feature and cannot be reverted. Start a new chat to use revert."} +``` + +**Fix:** Per-message revert only works for chats started after the feature shipped. Older sessions remain readable but their messages don't carry the marker the rewind needs. Start a new session for full revert support. + + +# Rate Limits +Source: https://docs.superdocs.app/errors/rate-limits + +Operation limits per subscription tier, overage pricing, and how to handle 429 responses. + +# Rate Limits + +SuperDocs uses operation-based limits, not request-rate limits. Each AI action that modifies, analyzes, or searches a document counts as one operation. + +## Limits by tier + +| Plan | Included operations | At the limit | Max monthly bill | +| -------------- | ------------------- | ---------------------------------------- | ---------------- | +| **Free** | 500/mo | Pauses (429 error) | \$0 | +| **Plus** | 2,000/mo | Overage at \$0.015/op, up to 4,000 total | \$50 | +| **Pro** | 10,000/mo | Overage at \$0.015/op, no cap | Varies | +| **Enterprise** | Custom | Custom | Custom | + +## Document scale limits + +Separate from operation quotas, the hosted service applies two scale limits so one pathological upload can never degrade the service for everyone: + +| Limit | Standard value | Error on crossing | +| ------------------------------------------------------- | ---------------------------- | ---------------------------- | +| Sections per document | 25,000 (roughly 6,000 pages) | `413` `DOCUMENT_TOO_COMPLEX` | +| Sections per chat session (all open documents combined) | 30,000 | `413` `SESSION_TOO_FULL` | + +These are stability limits, not platform caps. The platform itself has no hard limit on document length or on the number of documents open in parallel; the standard values exist to keep the shared hosted service fast and stable. Both errors are structured: the `detail` carries `section_count` / `max_sections` (or `session_section_count` / `max_session_sections`), `limit_expandable: true`, and `contact_email`. Hitting a limit notifies the team automatically; email [hello@superdocs.app](mailto:hello@superdocs.app) and your limits are raised, typically within a day. Larger dedicated deployments are available on Pro and Enterprise. + +## Handling 429 responses + +There are two kinds of `429`, and they need different handling. + +**1. Application 429 (quota or cooldown).** Returned as JSON with a `detail` field, and always carries a `Retry-After` header giving the number of seconds to wait before the request can succeed. When the cause is quota exhaustion (hitting the limit on the free plan), the response also carries the three `X-Usage-*` headers: + +``` +HTTP/1.1 429 Too Many Requests +Retry-After: 1209600 +X-Usage-Limit: 500 +X-Usage-Used: 500 +X-Usage-Remaining: 0 + +{"detail": "Your monthly operation quota (500) is exhausted. Upgrade to Pro for uninterrupted pay-as-you-go overage."} +``` + +Honor `Retry-After`: wait at least that many seconds before retrying. For a monthly quota it is the time until your billing cycle resets, so upgrading is usually the better path than waiting. Branch your client on the `429` status and the headers rather than string-matching the `detail` wording (the exact message may change). + +**2. Infrastructure 429 (traffic surge).** Under heavy platform load, requests can be throttled before they reach the application. These responses are plain text (for example `Rate exceeded`), not JSON, and carry no `Retry-After` header. Treat any non-JSON `429` as transient: retry with exponential backoff plus jitter. + +**Best practices:** + +* Check the `usage` object in chat responses to track remaining operations before you hit the limit +* On an application `429`, honor `Retry-After` and branch on the `X-Usage-Limit` / `X-Usage-Used` / `X-Usage-Remaining` headers. On successful responses, read the `usage` block in the JSON body (and the SSE `usage` event) instead; those headers are only present on the `429`. +* On a plain-text `429` with no `Retry-After`, back off exponentially with jitter and retry +* Upgrade to a paid plan for uninterrupted access + +## Check remaining operations + +### From API responses + +Every chat response includes usage data: + +```json theme={null} +{ + "usage": { + "monthly_used": 480, + "monthly_limit": 500, + "monthly_remaining": 20, + "was_billable": true, + "subscription_tier": "free" + } +} +``` + +### In the browser + +View your usage bar and tier limits at [use.superdocs.app](https://use.superdocs.app) GåÆ Settings. The `/v1/users/me/limits` and `/v1/users/me/usage` endpoints are web-app-only (they reject `sk_` / `lce_` keys with a `401`); from an API-key context, use the `usage` block in chat responses above. + +## Monthly reset + +Operations reset at the start of each billing cycle. Free plans reset monthly from account creation. Paid plans reset on each billing renewal date. + + +# curl Examples +Source: https://docs.superdocs.app/examples/curl + +Copy-paste ready curl commands for every key SuperDocs API operation. + +# curl Examples + +Replace `sk_YOUR_API_KEY` with your actual API key in all commands. + +## Chat + +### Send a message with document + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/chat \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "message": "Add a summary section at the top", + "session_id": "curl-demo", + "document_html": "

Report

Q4 revenue increased 15%.

" + }' +``` + +### Send a message without document + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/chat \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "message": "Draft a project proposal outline", + "session_id": "curl-demo" + }' +``` + +### Chat with model selection + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/chat \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "message": "Analyze this contract for potential risks", + "session_id": "curl-demo", + "document_html": "...", + "model_tier": "max", + "thinking_depth": "deep" + }' +``` + +## Async chat + +### Start an async request + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/chat/async \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "message": "Rewrite this document in formal language", + "session_id": "async-demo", + "document_html": "..." + }' +``` + +## Jobs + +### Check job status + +```bash theme={null} +curl https://api.superdocs.app/v1/jobs/JOB_ID \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +### List all jobs + +```bash theme={null} +curl https://api.superdocs.app/v1/jobs \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +### Cancel a job + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/jobs/JOB_ID/cancel \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +## Sessions + +### List sessions + +```bash theme={null} +curl https://api.superdocs.app/v1/sessions \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +### Get session history + +```bash theme={null} +curl https://api.superdocs.app/v1/sessions/SESSION_ID/history \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +### Get session jobs + +```bash theme={null} +curl https://api.superdocs.app/v1/sessions/SESSION_ID/jobs \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +### Delete a session + +Session deletion lives under `/v1/users/me/sessions/{id}` GÇö that endpoint is part of the web-app account surface and accepts Firebase tokens only, not `sk_` API keys. To clear sessions programmatically with an API key, simply stop using the `session_id` (sessions persist server-side but are not billed unless used). To delete from the UI, use **Settings > Sessions** in [use.superdocs.app](https://use.superdocs.app). + +## Documents + +### Upload a document to the editor + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/documents/upload \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -F "file=@contract.docx" \ + -F "session_id=my-session" +``` + +### Export a document + +```bash theme={null} +# From session GÇö DOCX (default format) +curl -X POST https://api.superdocs.app/v1/documents/export \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"session_id": "my-session", "format": "docx"}' \ + --output document.docx + +# From HTML GÇö PDF with paper and watermark options +curl -X POST https://api.superdocs.app/v1/documents/export \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "html": "

Title

Content

", + "format": "pdf", + "options": {"paper_size": "A4", "watermark_text": "DRAFT"} + }' \ + --output document.pdf +``` + +Formats: `docx` (default), `pdf`, `html`, `markdown`, `txt`. See [Exporting documents](/concepts/documents#exporting-documents) for the full options reference. + +## Attachments + +### Upload a file + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/attachments/upload \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -F "file=@document.pdf" \ + -F "session_id=my-session" +``` + +### Check attachment status + +```bash theme={null} +curl https://api.superdocs.app/v1/attachments/status/SESSION_ID \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +### Delete an attachment + +```bash theme={null} +curl -X DELETE "https://api.superdocs.app/v1/attachments/ATTACHMENT_ID?session_id=my-session" \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +## Templates + +### Upload a template + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/templates/upload \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -F "file=@template.docx" +``` + +### List templates + +```bash theme={null} +curl https://api.superdocs.app/v1/templates \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +### Delete a template + +```bash theme={null} +curl -X DELETE https://api.superdocs.app/v1/templates/TEMPLATE_ID \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +## HITL approval + +### Approve a single change + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/chat/SESSION_ID/approve \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "job_id": "JOB_ID", + "change_id": "CHANGE_ID", + "approved": true + }' +``` + +### Approve multiple changes + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/chat/SESSION_ID/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 + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/chat/SESSION_ID/approve \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "job_id": "JOB_ID", + "change_id": "CHANGE_ID", + "approved": false, + "feedback": "Keep the original wording but add a reference to GDPR" + }' +``` + +## User management + +The `/v1/users/*` endpoints (profile, usage, limits, sessions, programmatic API-key management) belong to the web-app account surface and accept **web-app session tokens only** GÇö `sk_` / `lce_` API keys are rejected with a `401`. View profile, usage, and tier limits from [use.superdocs.app](https://use.superdocs.app) GåÆ Settings. + +To check usage from an API-key context, the `usage` block is included in every `/v1/chat` and `/v1/chat/async` response (`monthly_used`, `monthly_limit`, `monthly_remaining`, `was_billable`, `subscription_tier`) and in every SSE `usage` event GÇö see [SSE Streaming](/guides/streaming). + +## Verify your API key + +The cheapest way to confirm an `sk_` key works: + +```bash theme={null} +curl https://api.superdocs.app/v1/sessions \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +A `200` with a JSON list of sessions (possibly empty) confirms the key. A `401` means the key is wrong, revoked, or the header didn't reach the server. **Do not use `/v1/users/me` to verify an API key** GÇö it's web-app-only and will return `401` even for a valid `sk_` key, which makes the key look bad when it isn't. + +## Health check + +```bash theme={null} +curl https://api.superdocs.app/health +``` + +REST `GET /health` requires no authentication. The MCP `health` tool is different: it runs over the authenticated `/mcp/` connection like every MCP tool, so a `401` there signals an API-key problem rather than downtime. + + +# JavaScript Examples +Source: https://docs.superdocs.app/examples/javascript + +Complete working JavaScript examples for the SuperDocs API using fetch and EventSource. + +# JavaScript Examples + +All examples use the native `fetch` API and work in browsers and Node.js 18+. + + + For other languages (Python, Go, Ruby, .NET) used in server-side / batch / queue-worker integrations, see [Server Integration](/guides/server-integration). For AI agent tool registration patterns (OpenAI, Anthropic, LangChain, LlamaIndex), see [Agent Tool Integration](/guides/agent-tool-integration). + + +## Basic chat with document + +```javascript theme={null} +const API_KEY = "sk_YOUR_API_KEY"; +const BASE = "https://api.superdocs.app"; + +async function chat(message, sessionId, documentHtml = null) { + const response = await fetch(`${BASE}/v1/chat`, { + method: "POST", + headers: { + "Authorization": `Bearer ${API_KEY}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + message, + session_id: sessionId, + document_html: documentHtml + }) + }); + + return response.json(); +} + +// Usage +const data = await chat( + "Add an introduction paragraph", + "js-demo", + "

My Document

Content here.

" +); + +console.log("AI:", data.response); +if (data.document_changes) { + console.log("Updated HTML:", data.document_changes.updated_html); +} +``` + +## EventSource streaming + +```javascript theme={null} +async function streamChat(message, sessionId, documentHtml) { + // 1. Start async job + const response = await fetch(`${BASE}/v1/chat/async`, { + method: "POST", + headers: { + "Authorization": `Bearer ${API_KEY}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + message, + session_id: sessionId, + document_html: documentHtml + }) + }); + + const { job_id } = await response.json(); + + // 2. Open SSE stream + return new Promise((resolve, reject) => { + const url = `${BASE}/v1/chat/${sessionId}/stream?job_id=${job_id}&api_key=${API_KEY}`; + const eventSource = new EventSource(url); + + eventSource.addEventListener("intermediate", (event) => { + const data = JSON.parse(event.data); + console.log("Progress:", data.content); + }); + + eventSource.addEventListener("final", (event) => { + const data = JSON.parse(event.data); + eventSource.close(); + resolve(data.result); + }); + + eventSource.addEventListener("usage", (event) => { + const data = JSON.parse(event.data); + console.log(`Operations: ${data.monthly_used}/${data.monthly_limit}`); + }); + + eventSource.addEventListener("error", (event) => { + eventSource.close(); + if (event.data) { + reject(new Error(JSON.parse(event.data).error)); + } + }); + }); +} + +// Usage +const result = await streamChat( + "Rewrite section 2 to be more formal", + "streaming-demo", + currentHtml +); +console.log("AI:", result.response); +``` + +## Upload document and export + +```javascript theme={null} +const API_KEY = "sk_YOUR_API_KEY"; +const BASE = "https://api.superdocs.app"; +const headers = { Authorization: `Bearer ${API_KEY}` }; + +// Upload a file as the active document +async function uploadDocument(file, sessionId) { + const formData = new FormData(); + formData.append("file", file); + formData.append("session_id", sessionId); + + const res = await fetch(`${BASE}/v1/documents/upload`, { + method: "POST", + headers: { Authorization: `Bearer ${API_KEY}` }, + body: formData, + }); + return res.json(); // { html, session_id, filename, chunks_count, version_id } +} + +// Export and download a document. Supported formats: "docx" (default), +// "pdf", "html", "markdown", "txt". +async function exportDocument(sessionId, format = "docx", options = {}) { + const res = await fetch(`${BASE}/v1/documents/export`, { + method: "POST", + headers: { ...headers, "Content-Type": "application/json" }, + body: JSON.stringify({ session_id: sessionId, format, options }), + }); + + // Surface any non-fatal warnings from the export pipeline + const warningsHeader = res.headers.get("X-Export-Warnings"); + if (warningsHeader) { + const warnings = JSON.parse(atob(warningsHeader)); + console.warn(`Export completed with ${warnings.length} warning(s):`, warnings); + } + + const blob = await res.blob(); + + // File extension mirrors the format value, with one exception + const ext = format === "markdown" ? "md" : format; + + // Trigger download in browser + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `document.${ext}`; + a.click(); + URL.revokeObjectURL(url); +} + +// Example: PDF export with paper-size override +await exportDocument("my-session", "pdf", { paper_size: "A4", watermark_text: "DRAFT" }); +``` + +See [Exporting documents](/concepts/documents#exporting-documents) for the full options reference and the upload-then-export pattern for documents over 20 MB. + +## File upload + +```javascript theme={null} +async function uploadAttachment(file, sessionId) { + const formData = new FormData(); + formData.append("file", file); + formData.append("session_id", sessionId); + + const response = await fetch(`${BASE}/v1/attachments/upload`, { + method: "POST", + headers: { "Authorization": `Bearer ${API_KEY}` }, + body: formData + }); + + return response.json(); +} + +// Browser usage +const fileInput = document.querySelector('input[type="file"]'); +const result = await uploadAttachment(fileInput.files[0], "my-session"); +console.log("Job ID:", result.job_id); +``` + +## Job polling + +```javascript theme={null} +async function pollJob(jobId) { + while (true) { + const response = await fetch(`${BASE}/v1/jobs/${jobId}`, { + headers: { "Authorization": `Bearer ${API_KEY}` } + }); + const job = await response.json(); + + if (job.status === "completed") return job.result; + if (job.status === "failed") throw new Error(job.error); + if (job.status === "awaiting_approval") return job; + + await new Promise(r => setTimeout(r, 2000)); + } +} + +// Usage +const { job_id } = await uploadAttachment(file, "my-session"); +const result = await pollJob(job_id); +console.log("Attachment processed:", result.attachment_id); +``` + +## HITL approval + +```javascript theme={null} +async function chatWithApproval(message, sessionId, documentHtml) { + // Start with approval mode + const response = await fetch(`${BASE}/v1/chat/async`, { + method: "POST", + headers: { + "Authorization": `Bearer ${API_KEY}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + message, + session_id: sessionId, + document_html: documentHtml, + approval_mode: "ask_every_time" + }) + }); + + const { job_id } = await response.json(); + + // Poll and approve + while (true) { + const jobResponse = await fetch(`${BASE}/v1/jobs/${job_id}`, { + headers: { "Authorization": `Bearer ${API_KEY}` } + }); + const job = await jobResponse.json(); + + if (job.status === "completed") return job.result; + if (job.status === "failed") throw new Error(job.error); + + if (job.status === "awaiting_approval") { + const changes = job.metadata.pending_changes; + console.log(`${changes.length} change(s) proposed`); + + // Auto-approve all (replace with UI in production) + await fetch(`${BASE}/v1/chat/${sessionId}/approve`, { + method: "POST", + headers: { + "Authorization": `Bearer ${API_KEY}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + job_id, + approved: true, + changes: changes.map(c => ({ change_id: c.change_id, approved: true })) + }) + }); + } + + await new Promise(r => setTimeout(r, 2000)); + } +} +``` + +## HITL approval with SSE streaming (real-time progress) + +The polling pattern above is fine for server-to-server flows where no human is watching. For interactive editor integrations GÇö where a user is staring at the chat panel waiting for their edit GÇö open the SSE stream alongside the async job so progress events arrive in real time and the UI never goes silent for more than a few seconds. + +```javascript theme={null} theme={null} +async function chatWithApprovalAndStreaming(message, sessionId, documentHtml, callbacks) { + // 1. Start async job + const r = await fetch(`${BASE}/v1/chat/async`, { + method: "POST", + headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" }, + body: JSON.stringify({ + message, + session_id: sessionId, + document_html: documentHtml, + approval_mode: "ask_every_time", + }), + }); + const { job_id } = await r.json(); + + // 2. Open SSE stream and surface every event to the UI in real time + return new Promise((resolve, reject) => { + const es = new EventSource( + `${BASE}/v1/chat/${sessionId}/stream?job_id=${job_id}&api_key=${API_KEY}` + ); + + // Render in-flight progress so the UI never looks frozen + es.addEventListener("intermediate", (event) => { + const data = JSON.parse(event.data); + callbacks.onProgress?.(data.content); + }); + + // The whole batch of proposed changes arrives in one event GÇö even a + // single-change turn comes through here as a one-element changes[] array. + // (You may also register "proposed_change" defensively, but it never fires + // today GÇö proposed_change_batch is the event that's emitted.) + es.addEventListener("proposed_change_batch", (event) => { + const envelope = JSON.parse(event.data); + const batch = JSON.parse(envelope.content); // double-parse GÇö required + // Surface every change in the batch for review. + for (const change of batch.changes) { + callbacks.onProposedChange?.(change, async (decision) => { + // Send this change's approve/deny decision back. SSE stays open through the cycle. + await fetch(`${BASE}/v1/chat/${sessionId}/approve`, { + method: "POST", + headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" }, + body: JSON.stringify({ + job_id, + approved: true, + changes: [{ change_id: change.change_id, ...decision }], + }), + }); + }); + } + }); + + es.addEventListener("final", (event) => { + es.close(); + resolve(JSON.parse(event.data)); + }); + + es.addEventListener("error", (event) => { + es.close(); + reject(new Error(event.data ? JSON.parse(event.data).error : "SSE stream error")); + }); + }); +} + +// Usage in a chat UI: +chatWithApprovalAndStreaming( + "Tighten the OBLIGATIONS section", + "session-abc", + document.querySelector("#editor").innerHTML, + { + onProgress: (text) => { + // Update the in-flight chat bubble GÇö see Streaming guide for the bubble pattern + updateInFlightBubble(text); + }, + onProposedChange: (change, respond) => { + showInlineDiffCard({ + chunkId: change.chunk_id, + oldHtml: change.old_html, + newHtml: change.new_html, + explanation: change.ai_explanation, + onApprove: () => respond({ approved: true }), + onDeny: (feedback) => respond({ approved: false, feedback }), + }); + }, + } +).then((result) => { + // Replace the in-flight bubble with the final response, swap in result.updated_html if present + finalizeBubble(result); +}); +``` + +**When to use streaming vs polling.** + +* **Streaming** GÇö interactive editor UIs, any flow where a user is watching. Users see progress text within 1GÇô5 seconds, and the turn's proposed changes arrive together in one `proposed_change_batch` event ready to render as a review list. +* **Polling** GÇö background batch processing, server-to-server flows, queues where the user isn't waiting. Simpler to implement; perfectly fine when latency feedback isn't a UX requirement. + +See [Streaming GåÆ Rendering intermediate events](/guides/streaming#rendering-intermediate-events-in-your-ui) for the in-flight bubble pattern, and [Async jobs GåÆ Latency expectations](/guides/async-jobs#latency-expectations-and-timeout-strategy) for typical operation durations. + +## Frontend layout + +Most products that integrate SuperDocs converge on a two-pane layout: the document editor on the left (primary area) and a chat panel on the right (fixed width, 380GÇô440 px). The skeleton below is framework-agnostic GÇö the same shape works in React, Vue, Svelte, or plain HTML. + +```html theme={null} theme={null} + +
+
+ Your App + session: my-session + 42 / 500 ops + +
+ +
+ +
+ + +
+ + +``` + +Notes on the pattern: + +* **Inline diff overlays** render inside the editor pane (see [Rendering diffs inline in your editor](/guides/human-in-the-loop#rendering-diffs-inline-in-your-editor)). The chat pane only holds the message log and the batch-approve bar. +* **The chat panel reads editor HTML before every send** and writes it back on every `final` event. Expose this through a ref / store so the chat doesn't have to know about your editor's internals. +* **One session per tab.** Generate a random `session_id` on page load and reuse it for every request in that tab. SuperDocs persists conversations across server restarts. +* **API key stays server-side.** If you're building a browser app, proxy SuperDocs calls through your own backend so `sk_...` never reaches the browser. See the [streaming guide](/guides/streaming) for the `api_key` query-parameter workaround for `EventSource`, which cannot set custom headers. + + +# Python Examples +Source: https://docs.superdocs.app/examples/python + +Complete working Python examples for the SuperDocs API using the requests library. + +# Python Examples + +All examples use the `requests` library. Install it with `pip install requests`. + +## Basic chat with document + +```python theme={null} +import requests + +API_KEY = "sk_YOUR_API_KEY" +BASE = "https://api.superdocs.app" +HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"} + +response = requests.post(f"{BASE}/v1/chat", headers=HEADERS, json={ + "message": "Add an executive summary at the top", + "session_id": "python-demo", + "document_html": "

Q4 Report

Revenue grew 15% year over year.

" +}) + +data = response.json() +print("AI:", data["response"]) + +if data.get("document_changes"): + updated_html = data["document_changes"]["updated_html"] + print("Document updated. Version:", data["document_changes"]["version_id"]) +``` + +## Handle response and preserve HTML + +```python theme={null} +# First request +response = requests.post(f"{BASE}/v1/chat", headers=HEADERS, json={ + "message": "Create a project plan with 3 sections", + "session_id": "project-plan", + "document_html": "" +}) +data = response.json() +current_html = data["document_changes"]["updated_html"] + +# Second request GÇö send the HTML back as-is +response = requests.post(f"{BASE}/v1/chat", headers=HEADERS, json={ + "message": "Add deadlines to each section", + "session_id": "project-plan", + "document_html": current_html # preserve data-chunk-id attributes +}) +data = response.json() +current_html = data["document_changes"]["updated_html"] +``` + +## Upload document and edit it + +```python theme={null} +# Upload a file as the active document +upload = requests.post(f"{BASE}/v1/documents/upload", + headers={"Authorization": f"Bearer {API_KEY}"}, + files={"file": open("contract.docx", "rb")}, + data={"session_id": "doc-edit"} +) +doc = upload.json() +print(f"Loaded {doc['filename']} ({doc['chunks_count']} sections)") + +# Edit it with AI +response = requests.post(f"{BASE}/v1/chat", headers=HEADERS, json={ + "message": "Simplify the language throughout", + "session_id": "doc-edit", + "document_html": doc["html"] +}) +updated_html = response.json()["document_changes"]["updated_html"] +``` + +## Export a document + +```python theme={null} +# Export from a session as DOCX (default format) +response = requests.post(f"{BASE}/v1/documents/export", headers=HEADERS, json={ + "session_id": "doc-edit", + "format": "docx", + "options": {"filename": "simplified-contract"}, +}) + +with open("simplified-contract.docx", "wb") as f: + f.write(response.content) + +# Export from HTML directly as PDF with A4 paper and a watermark +response = requests.post(f"{BASE}/v1/documents/export", headers=HEADERS, json={ + "html": updated_html, + "format": "pdf", + "options": { + "paper_size": "A4", + "watermark_text": "DRAFT", + }, +}) + +with open("export.pdf", "wb") as f: + f.write(response.content) +``` + +Supported formats: `docx` (default), `pdf`, `html`, `markdown`, `txt`. See [Exporting documents](/concepts/documents#exporting-documents) for the full options reference and large-document workflows. + +## Upload attachment and poll for completion + +```python theme={null} +import time + +# Upload +with open("reference.pdf", "rb") as f: + response = requests.post( + f"{BASE}/v1/attachments/upload", + headers={"Authorization": f"Bearer {API_KEY}"}, + files={"file": ("reference.pdf", f, "application/pdf")}, + data={"session_id": "my-session"} + ) + +job_id = response.json()["job_id"] +print(f"Upload started: {job_id}") + +# Poll until ready +while True: + job = requests.get(f"{BASE}/v1/jobs/{job_id}", headers=HEADERS).json() + if job["status"] == "completed": + print("Attachment ready") + break + elif job["status"] == "failed": + print("Processing failed:", job["error"]) + break + time.sleep(2) + +# Now chat GÇö the AI can reference the attachment +response = requests.post(f"{BASE}/v1/chat", headers=HEADERS, json={ + "message": "Summarize the key points from the uploaded reference document", + "session_id": "my-session" +}) +print("AI:", response.json()["response"]) +``` + +## Async job with polling + +```python theme={null} +import time + +# Start async +response = requests.post(f"{BASE}/v1/chat/async", headers=HEADERS, json={ + "message": "Rewrite this entire document in formal legal language", + "session_id": "legal-rewrite", + "document_html": "

Agreement

We agree to work together on the project.

" +}) +job_id = response.json()["job_id"] + +# Poll +while True: + job = requests.get(f"{BASE}/v1/jobs/{job_id}", headers=HEADERS).json() + print(f"Status: {job['status']}") + + if job["status"] == "completed": + result = job["result"] + print("AI:", result["response"]) + if result.get("document_changes"): + print("Updated HTML:", result["document_changes"]["updated_html"][:200]) + break + elif job["status"] == "failed": + print("Error:", job["error"]) + break + + time.sleep(2) +``` + +## SSE streaming + +```python theme={null} +import json +import requests + +# Start async job first +response = requests.post(f"{BASE}/v1/chat/async", headers=HEADERS, json={ + "message": "Write a detailed analysis of section 2", + "session_id": "streaming-demo", + "document_html": "..." +}) +job_id = response.json()["job_id"] + +# Stream progress +url = f"{BASE}/v1/chat/streaming-demo/stream?job_id={job_id}&api_key={API_KEY}" +with requests.get(url, stream=True) as stream: + current_event = None + for line in stream.iter_lines(): + if not line: + continue + decoded = line.decode("utf-8") + if decoded.startswith("event: "): + current_event = decoded[7:] + elif decoded.startswith("data: "): + data = json.loads(decoded[6:]) + if current_event == "intermediate": + print(f"Progress: {data['content']}") + elif current_event == "final": + print(f"Done: {data['result']['response'][:100]}") + break + elif current_event == "error": + print(f"Error: {data['error']}") + break +``` + +## HITL approval flow + +```python theme={null} +import time + +# Start with approval mode +response = requests.post(f"{BASE}/v1/chat/async", headers=HEADERS, json={ + "message": "Update the liability clause to cap damages at $1M", + "session_id": "contract-review", + "document_html": "...", + "approval_mode": "ask_every_time" +}) +job_id = response.json()["job_id"] + +# Poll and approve +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"] + print(f"\n{len(changes)} change(s) proposed:") + for c in changes: + print(f" - {c['operation']}: {c.get('ai_explanation', 'No explanation')}") + + # Approve all + 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("Approved all changes") + + time.sleep(2) +``` + + +# Features +Source: https://docs.superdocs.app/features + +Comprehensive capability reference for SuperDocs GÇö every shipped feature, grouped by what you'd reach for it for. + +# Features + +SuperDocs is a universal AI document platform GÇö one API for editing, drafting, searching, summarizing, generating visual content (images, diagrams, drawings, equations), and exporting styled documents (`.docx`, PDF, HTML, Markdown, plain text). Every capability below is live in production at `https://api.superdocs.app` and exposed identically over the REST API and the MCP server at `https://api.superdocs.app/mcp/` (38 tools + 4 user-invocable workflow prompts on a single endpoint). + +This page is exhaustive GÇö it's a reference, not a marketing summary. Skip to the group that matches what you're trying to build. + +*** + +## Core editing intelligence + +### Section-precision editing + +Documents load as structured HTML where every paragraph, heading, table, row, and cell carries a unique identifier internally. The AI can target a specific section without touching anything else GÇö **"remove row 3 of the pricing table"**, **"bold the second paragraph in section 4"**, **"replace the governing-law clause"** all work as natural-language instructions. + +This survives across multi-turn editing, so a 100-page document edited 30 times still has the same coherent structure at the end as it did at the start. Plain-text rewrites lose this completely. + +**Best for:** Long documents, contracts, SOPs, formal reports GÇö anywhere targeted edits matter more than wholesale rewrites. + +### Style preservation on edit and export + +Tables (with borders, alternating row shading, merged cells), fonts, font sizes, colors, inline styling, lists, indentation, and headers/footers all survive both AI edits AND the round-trip to `.docx` / PDF / HTML. Most general-purpose AI tools strip or mangle these GÇö SuperDocs is built around preserving them. + +**Best for:** Branded templates, legal documents, formal correspondence GÇö anywhere format fidelity is part of the deliverable. + +*** + +## Document intelligence + +### Search within documents + +Search isn't just full-text matching GÇö the AI understands semantic meaning. Ask **"find all indemnity clauses"** across a 100-page contract and get back the exact sections, even when they use different wording. Works against the active document or across all attachments at once. Results come back with surrounding context so you can verify the match without opening the file. + +**Best for:** Contract review, compliance audits, extracting specific terms from long documents, finding all references to a concept across multiple files. + +### Summarize sections on demand + +Ask **"summarize the force majeure section"** or **"give me a 2-line summary of the payment terms"** and the AI extracts just that section, then summarizes it at the level of detail you want. Pair with semantic search GÇö **"find and summarize all limitation-of-liability clauses"** works as a single instruction. No need to read line-by-line. + +**Best for:** Contract reviews, due diligence, quick understanding of specific parts of long documents, briefing materials. + +### Summarize entire documents at any length + +Get a concise summary of an entire document at a length you specify. **"Executive summary in three sentences"**, **"one-page overview"**, **"detailed bullet-point summary by section"** GÇö all work. The AI reads the full document and distills it efficiently. Summaries cover key sections, critical terms, and overall structure. Works on documents up to \~100 pages without context-window pressure. + +**Best for:** Quick understanding of newly-received documents, briefing executives, preparing for negotiations, extracting key terms from regulatory filings. + +### Cross-document reference and synthesis + +Upload multiple documents into a session and the AI references any of them while editing the active document. **"Port the indemnity clause from the attached template into this contract"**, **"compare these two NDAs and align the payment terms"**, **"find clauses in the attached precedent that handle this case better than what's currently in the draft"** GÇö all work as natural-language instructions. The AI searches across all attachments, surfaces matches, and adapts content to the current context. + +**Best for:** Standardizing language across contracts, porting terms from templates, comparing versions, drafting new docs based on prior precedents, building from clause libraries. + +*** + +## Rich editing & formatting + +### Full rich-text formatting toolbar + +Beyond basic editing GÇö the AI applies bold, italic, underline, strikethrough, text color (10-color palette), text highlight, custom font selection, six preset font sizes (12pxGÇô30px), configurable line spacing (single, 1.15, 1.5, double), and text alignment (left, center, right, justify). All preserved on round-trip exports. The format painter lets you copy a style from one selection and apply it to another with a single click GÇö useful for matching styling across a long document. + +**Best for:** Visually distinctive documents, applying brand style guides, adapting documents for different audiences, preserving template styling on edit. + +### Heading and list hierarchies + +Apply heading styles (H1, H2, H3) with hierarchy preserved on export. Create bulleted and numbered lists with automatic numbering. Nest lists up to 8 levels deep. Convert between bullet and numbered formats by natural-language instruction. Add blockquotes for citations or emphasis. Horizontal rules to divide sections. The AI respects these structures GÇö **"convert the first three paragraphs to a bulleted list"** works without reformatting surrounding content. + +**Best for:** SOPs, structured documentation, proposals, outlines, academic papers, anywhere hierarchy matters. + +### Table editing with cell-level control + +Insert and delete tables and rows. Merge or split cells. Apply alternating row shading. Set border styles. All cell-level operations work via natural language: **"merge the first three columns of row 2"**, **"add a row after row 4 with these values"**, **"split the merged cell in A1"**. Tables survive round-trip to `.docx` and PDF with formatting intact. Useful for pricing tables, comparison matrices, and any data-heavy document. + +**Best for:** Documents with pricing or data GÇö proposals, contracts with pricing schedules, technical specifications, comparison charts. + +### Headers, footers, footnotes, comments, and hyperlinks + +Add headers and footers that appear on every page (or on the first page / odd / even pages only) GÇö page numbers, document titles, company names, dates. Hyperlinks work the same way: **"make this text link to our website"** or **"remove the broken link in section 4"**. Both internal document references and external URLs supported. + +**Best for:** Formal business documents, branded templates, compliance documents requiring page numbering, documents with cross-references or call-to-action links. + +### Edit headers, footers, footnotes, and comments by chat + +Headers, footers, footnotes, endnotes, and comments are first-class parts of the document, not flattened into the body GÇö and you can edit any of them by chatting. **"Change the footer to include the confidentiality notice"**, **"fix the typo in footnote 3"**, **"update the running header on the first page"**, **"add a comment on the payment clause asking legal to review it"** all work as natural-language instructions. + +These edits ride the **same review flow** as body-content edits: a single turn can propose both a body change and a header change, and in human-in-the-loop mode each arrives as its own before/after entry in the turn's review card so you approve them independently. Because parts live alongside the document, they survive edits, revert, multi-document sessions, and the round-trip to `.docx` and PDF GÇö a footnote you edit stays a real Word footnote on export, not inline text. + +**Best for:** Contracts and formal reports where footnotes and reviewer comments carry legal weight, branded templates with per-section running headers, editorial workflows where notes and comments are part of the deliverable. + +*** + +## Visual content & media + +Generate, edit, and embed images, diagrams, drawings, and equations directly inside your document GÇö all by chatting. Every operation rides the same chat interface; no separate tools or modes to learn. + +### Generate images from a description + +Describe the image you want and the AI creates it and inserts it into the document. Photorealistic shots, illustrations, hero images, logos, infographics with text GÇö all by natural-language instruction. The AI picks the right generation model automatically: a photorealistic image generator by default, and a text-rendering-optimized model for prompts that need legible text on the image (logos, infographics, slide hero shots, anything where letterforms matter more than photographic realism). + +Generated images live in the document like any other photo GÇö they can be edited, replaced, or removed afterward by chat. + +**Best for:** Marketing pages, proposals, sales decks, blog posts, product specs, anything where finding stock imagery is friction. + +### Edit any image with natural language + +Click any image in the editor and tell the AI what to change. Supported edits include brightness, darkness, contrast, recolor (e.g., "warm sepia", "cooler blues"), circular mask with transparent corners, tight crop to the subject, semi-transparent fade, and full background removal GÇö plus anything else you'd describe in plain English to a photo editor. + +The AI generates new bytes and swaps them in. Subject identity (faces, geometry, branding) is preserved across the edit. Each edit takes about 20 seconds. You can iterate (**"a bit warmer"**, **"now crop tighter"**) and use **Reset to Original** to revert to the original bytes at any time. + +**Best for:** Marketing assets that need to match a campaign, reports and contracts where logos need a quick cleanup, design docs where reference shots need treatment, anywhere a small photo edit would otherwise mean leaving the document. + +### Replace an image with an attachment + +Attach a fresh image to chat (the new logo, the updated headshot, the corrected diagram screenshot) and ask the AI to swap it in for an existing image in the document. Pure URL swap GÇö no AI generation cost, no waiting. Works for one-off updates (**"replace the cover photo with this"**) and for recurring patterns where users supply a fresh asset on every iteration of the document. + +**Best for:** Branded templates that need per-customer assets, document workflows where users supply their own imagery, content updates after a rebrand. + +### Auto-rendered diagrams (Mermaid) + +Describe a flowchart, sequence diagram, org chart, ER diagram, mindmap, timeline, or Gantt and the AI emits the diagram code; the editor live-renders it as a clean SVG. Re-editing is instant and free of regeneration cost GÇö say **"add a Police Verification step after Background Check"** and the AI rewrites the diagram in place rather than regenerating the whole image. + +Diagrams export correctly in PDF and `.docx` (rendered to image at export time). The full Mermaid syntax catalog is supported GÇö flowchart, sequence, class, state, ER, user journey, gantt, pie, mindmap, timeline, xychart. + +**Best for:** Technical documentation, process docs, runbooks, architecture overviews, training materials GÇö anywhere a diagram beats a paragraph. + +### Hand-drawn sketches with a built-in canvas + +Open a freehand drawing canvas inside the editor GÇö sketch quickly with shapes, arrows, and freehand strokes, save, and the drawing lands in the document as an image. Click it later, hit **Redraw**, and the canvas reopens with your original strokes so you can tweak them. + +The same drawing can also be edited as an image via natural language (**"turn this rough sketch into a realistic logo"**) GÇö and the original strokes are still kept on the image, so you can redraw from scratch even after AI editing. + +**Best for:** Whiteboarding inside a doc, quick architecture sketches, annotating a screenshot, turning a hand-sketched logo into something polished without leaving the editor. + +### Math equations (LaTeX / KaTeX) + +Insert math equations using familiar LaTeX syntax GÇö inline (`$x^2$`) or block (`$$x^2 + y^2 = z^2$$`). The editor live-renders them with KaTeX. Equations survive `.docx` and PDF export. + +The toolbar has an equation button that prompts for LaTeX so you don't have to remember the delimiters; in chat you can also just say **"add the quadratic formula here"** and the AI will insert the right LaTeX in place. + +**Best for:** Academic papers, technical reports, finance documents with formulas, engineering specs, anywhere math notation needs to be written correctly the first time. + +### Auto-generated table of contents + +Drop a table-of-contents block into the document and it auto-populates from your H1/H2/H3 headings. Add or rename a heading later GÇö the TOC updates itself. Click a TOC entry to jump to that section. Survives export to PDF and `.docx` (rendered as a list of section titles). + +**Best for:** Long documents, manuals, books, regulatory filings GÇö anywhere 10+ pages where readers need to navigate. + +*** + +## Knowledge & attachments + +### Multimodal vision on attachments + +Attach images (PNG, JPG, GIF, WebP), screenshots, scanned forms, diagrams, and charts GÇö the AI interprets them visually while editing the active document. Transcribe a screenshot into structured text, extract numbers from a chart, reference a diagram while drafting documentation, identify entities in a scanned form. + +**Best for:** Workflows that mix text documents with image references GÇö design docs, compliance docs that reference scanned forms, technical writing that references architecture diagrams. + +### Build a knowledge base from attachments + +Upload your organization's template library, style guides, past contracts, or SOP documents as attachments. The AI references them automatically when editing the active document GÇö **"make this sound like our standard tone"**, **"follow the format of our usual NDAs"**, **"use the indemnity language from our gold-standard contract"**. Builds an institutional memory that shapes AI behavior. Attachments are scoped per session, so a knowledge-base session can be reused as a starting point and copied for each new document. + +**Best for:** B2B teams with house styles or templates, legal teams with clause libraries, marketing teams with brand voice, compliance teams with corporate policies. + +### Semantic search across attachments + +Every attachment is semantically indexed when uploaded GÇö the AI knows what's in it, not just the words it contains. Ask **"find the data processing clause from the attached regulations"** and the search returns the matching section even when the attached doc uses entirely different wording. Useful for large attachments (50+ pages) where keyword search would miss relevant sections, and for reference documents you query repeatedly across sessions. + +**Best for:** Reference docs (regulations, industry standards, policy manuals), large attachments queried multiple times, building searchable knowledge over time. + +*** + +## Conversation & robustness + +### Multiple open documents in one session + +A session can hold several open documents at once GÇö like editor tabs. The AI sees the full roster, edits whichever document your request targets (explicit `document_id`, named in the message, or the focused one by default), reads or searches across all of them, moves content between them, and can open a brand-new document on request ("put the summary in a new document"). Uploads choose their behavior via `open_mode` (`replace` / `new_focused` / `background`), and three endpoints manage the roster (list / focus / close) GÇö also exposed as MCP tools. See the [Multi-Document Sessions guide](/guides/multi-document). + +**Best for:** invoice + rate card workflows, contract + amendment pairs, splitting AI output into a separate deliverable, any flow where one conversation spans several files. + +### Persistent conversation context + +Every message in a session carries the full conversation history. Ask **"make that more formal"**, then **"add three paragraphs of detail to what you just wrote"**, then **"change the tone back to friendly"** GÇö the AI remembers all prior context, edits, and stated preferences. History persists across server restarts and redeploys. Reload a session weeks later and the AI still has the full context (the document, the attachments, every turn of the conversation, every change made). + +**Best for:** Iterative editing workflows, multi-turn refinements, long-running document projects, anything where the user comes back to continue work later. + +### Automatic error recovery with fallbacks + +When an edit fails on the first approach (e.g., the section the AI tried to find didn't exist with those exact keywords), it automatically tries broader strategies GÇö semantic search instead of keyword match, alternative phrasings of the user intent, looking in adjacent sections. Retries happen without user intervention. If all approaches genuinely fail, the AI explains what it tried and asks for clarification with specifics rather than a generic error. Dramatically reduces "I didn't understand your request" loops. + +**Best for:** Complex documents with varied terminology, ambiguous instructions, long documents where sections may have been edited since last viewed. + +*** + +## Human control & approval + +### Human-in-the-loop approval + +For sensitive edits (legal contracts, financial filings, anything user-facing), set `approval_mode='ask_every_time'` on `chat_async` and the agent surfaces each proposed change as a structured diff (chunk-level before/after HTML + an explanation) for the user to approve, deny, or send back with feedback. Approved changes apply atomically; denied changes leave the document untouched. State persists across server restarts so multi-step approvals survive autoscaling. + +**Best for:** Multi-stakeholder workflows, regulated industries, anywhere the cost of a bad edit landing without review exceeds the cost of a confirmation click. + +### Compact response mode for long editing sessions + +On documents larger than \~20 pages, the default `chat` response includes the entire updated HTML on every turn GÇö that's \~130K tokens for a 100-page styled doc. Set `response_mode='compact'` and the response includes only `chunk_diffs` (per-section before/after for sections that actually changed) GÇö typically 1-3 chunks, \~500-2,000 tokens. + +For a 5-turn editing session on a 100-page doc, compact mode reduces total response context from \~650K tokens to \~3K tokens. To read sections in compact mode, just ask in natural language GÇö **"show me the force majeure clause"** returns the content in the chat reply text. + +**Best for:** AI agents editing documents larger than \~20 pages where context window pressure matters. + +### Real-time progress via SSE + +Subscribe to `/v1/chat/{session_id}/stream` to receive intermediate progress events while the agent works GÇö `intermediate` (status updates), `proposed_change_batch` (HITL diff for a turn's proposed changes delivered as one event GÇö used even for a single change; header / footer / footnote / comment edits arrive here too, each as its own entry), `document_sync` (chunk-ID sync after upload), `continue_prompt` (a large edit paused and is waiting for you to continue or stop), `documents_changed` (which documents an auto-applied turn touched, for multi-document sessions), `model_fallback` (the service automatically failed over to a different model tier), `final` (completed response), `usage` (operation count + tokens), `error`. Auto-reconnect on drop. + +**Best for:** Frontends that show live progress, status bars, or partial results before the full answer is ready. + +*** + +## Multi-format I/O + +### Multi-format input and output + +| Input formats | Output formats | +| ------------------------------------------------------------------------------------------------- | ---------------------------------------- | +| `.docx`, `.doc`, `.odt`, `.rtf`, `.tex` (+ LaTeX project `.zip`), PDF, HTML, Markdown, plain text | `.docx`, PDF, HTML, Markdown, plain text | + +Same parsing pipeline regardless of input GÇö once a document is loaded into a session, every editing tool works identically. Legacy Word (`.doc`), OpenDocument (`.odt`), and Rich Text (`.rtf`) files are normalized into the same rich pipeline as `.docx`, so headings, tables, and styling carry through. LaTeX uploads (single `.tex` files or whole project archives with chapters, figures, and bibliographies) import with lossless math, real footnotes, rendered citations, and source-true page geometry GÇö see [Attachments](/concepts/attachments) for the details. Export from any session in any of the five output formats, with per-export customisation for paper size, orientation, margins, filename, image embedding, and optional PDF watermarks. Documents above 20 MB use a pre-signed upload pattern to bypass standard request body limits; documents above 100 MB are rendered asynchronously and delivered via email. + +**Best for:** Format conversion workflows (HTML GåÆ styled `.docx`, Markdown GåÆ PDF, `.docx` GåÆ Markdown), accepting user uploads in whatever format they have, or producing print-ready PDFs with custom paper sizes and watermarks. + +### High-fidelity export + +Exports reproduce the real structure of a document, not a flattened approximation. On `.docx` export, footnotes and endnotes come back as genuine Word footnotes/endnotes, comments as real Word comments, headers and footers as real header/footer parts (with live page-number and page-count fields), equations as native Word math you can keep editing in Word (not pictures of math), and page geometry GÇö margins, orientation, section breaks, and multi-column layout GÇö is preserved section by section. On PDF export, headers and footers render in the page margins with working page numbers, each section keeps its own page geometry, and math renders as typeset equations. Math (LaTeX), diagrams, and drawings round-trip in both formats. Bookmarks and internal cross-reference targets survive upload, editing, and export, and external hyperlinks in uploaded `.docx` files stay live links. + +Export defaults to a high-fidelity mode that preserves the source formatting faithfully (for example, it won't impose table borders or full-width tables that weren't in your document). A per-request `fidelity` option lets you choose `strict` (the faithful default) or `compat` (the previous rendering behaviour) if you need the older output for an existing pipeline. + +**Best for:** Legal and financial documents where footnotes, comments, and page layout are part of the record; branded templates that must survive the round-trip exactly; academic papers with equations and multi-column layouts. + +### High-fidelity PDF import + +PDFs upload with their full content preserved GÇö page text, **annotations** (sticky notes, highlights, FreeText callouts, reviewer comments), and **embedded images**. Every annotation is visible to the AI with the original author and date attached, so editing instructions like **"apply the corrections from the auditor's notes"** or **"reflect the reviewer's comments in section 3"** work natively. Embedded images (logos, photos, diagrams) become full first-class images you can replace, edit, or reference visually GÇö they don't get silently dropped to text-only. + +**Best for:** Compliance and audit workflows where PDFs carry reviewer annotations; brand and marketing collateral PDFs where embedded photos must round-trip; any enterprise GRC review cycle that uses annotated PDFs. + +### Scanned and complex-layout PDFs + +Scanned or image-only PDFs GÇö where the pages are pictures with no selectable text GÇö go through text recognition and come back as editable text. PDFs with harder layouts (multiple text columns, dense ruled tables) are read with their reading order and table structure preserved rather than collapsed into a jumble GÇö headings, links, tables, and images come through as structured, editable content, and hyphenated line breaks are rejoined automatically. + +**Best for:** Digitizing scanned contracts and forms, working with academic papers and reports in multi-column layouts, extracting tables from data-heavy PDFs. + +### Word track-changes import + +Word documents uploaded with **Track Changes** enabled keep all of it on import GÇö insertions, deletions, comments GÇö each tagged with the reviewer's name and date. The AI sees who edited what and can act on it: **"apply all the proofreader's insertions"**, **"reject the deletions in section 2"**, **"address the comments from the legal reviewer"**. Editorial workflows that depend on review markup (academic supervisor edits, legal redlines, teacher-corrected student work) translate straight from Word into SuperDocs. + +**Best for:** Editorial review cycles, redline workflows, teacher-student document review, multi-stakeholder review where preserving the trail of who-changed-what matters. + +### Reliable multi-file attachments + +Attach more than one file in the same message and reference each by filename GÇö **"compare the clauses in `contract_v2.pdf` against `contract_v1.pdf`"** or **"merge `outline.txt` into `proposal.docx`"**. Filename-based lookup is exact (case-insensitive, partial match) so the AI doesn't have to guess from content similarity which file you mean. + +**Best for:** Document comparison and merge workflows, multi-file review, proposal assembly from supporting attachments. + +### Document length awareness + +Ask **"how many pages is this?"** and get a real answer GÇö exact for paginated formats (`.docx`, PDF) and a clearly-labelled approximation ("approximately N pages") for unpaginated formats (HTML, Markdown, plain text). The page count is available as part of attachment metadata so any agent can read it without an extra round trip. + +**Best for:** Length-sensitive workflows (briefs with strict page limits, summaries that target a length, budgeting AI work against document size). + +### Pre-signed URL upload and download + +For files larger than \~100KB, the agent gets a 5-minute pre-signed PUT URL plus a ready-to-run curl example. The agent shells the file directly to cloud storage GÇö bytes never pass through the agent's context window. Same pattern in reverse for downloads (15-minute GET URL). + +For a 100-page styled `.docx`, this saves the agent \~70K tokens per upload (vs base64 inline). Five turns of editing on the same document drops from \~700K tokens of context overhead to \~3K tokens with the matching `response_mode='compact'` on chat. + +**Best for:** Production AI agents working with real-world document sizes (multi-page contracts, manuals, regulatory filings). Max file size 100 MB; up to 25,000 sections per document and 30,000 per session as standard, expandable on request (stability limits, not platform caps). + +*** + +## Developer & integration + +### MCP server GÇö 38 tools, every major client + +The same capabilities are exposed as a Model Context Protocol server at `https://api.superdocs.app/mcp/` (Streamable HTTP transport). Compatible clients render the 38 tools natively: + +| Client | Status | +| --------------------------------------- | ----------------- | +| Claude Code | Tools + Prompts G£ô | +| Claude Desktop | Tools + Prompts G£ô | +| Cursor | Tools + Prompts G£ô | +| VS Code (GitHub Copilot) | Tools + Prompts G£ô | +| Zed, Continue, Amazon Q CLI, and others | Tools + Prompts G£ô | +| Windsurf, Cline | Tools only | + +The same MCP server also exposes 4 user-invocable workflow templates (surfaced as `/superdocs:edit_styled_docx`, `/superdocs:convert_format`, etc. in clients that render MCP prompts as slash commands). One MCP server entry in your client config; both tools and prompts come together. + +**Best for:** AI-coding-tool users who want SuperDocs available in their editor without writing API integration code. + +### Three authentication paths + +| Method | Best for | +| ------------------------------ | ------------------------------------------------------------------------------------- | +| Web app login | The web app at `use.superdocs.app` (auto-issued via Google Sign-In or email/password) | +| User API key (`sk_GǪ`) | Individual developers, MCP integrations, scripts | +| Organization API key (`lce_GǪ`) | B2B integrations with shared usage limits | + +All three reach the same 38 MCP tools and the same REST API with identical scoping. Users own and manage their own keys; orgs manage theirs separately. + +### REST API works with any programming language + +One REST API works with any language that can make HTTP requests. No NPM packages to keep current, no language-specific SDKs to maintain, no framework dependencies to upgrade. Integrate with a few lines of code in whatever language your backend already speaks. Full OpenAPI specification published GÇö generate your own typed client with `openapi-generator`, Stainless, or any other codegen tool of your choice if you want type hints in your editor. + +**Best for:** Polyglot engineering teams, backend services across multiple languages, avoiding SDK lock-in, simple integrations that don't warrant a full SDK. + +### Real-time usage tracking and transparency + +Every API response includes usage data GÇö operation count consumed, tokens used. The SSE stream emits a `usage` event after each operation. Your dashboard shows remaining operations in your current tier and resets on your billing cycle. Promo allowances deplete before paid-tier operations so you always burn the cheapest credits first. Full transparency at every step GÇö no hidden costs, no monthly surprises, no need to call sales for usage data. + +**Best for:** Cost-conscious integrations, monitoring spend, forecasting overage, teams on a budget, self-service deployments at scale. + +*** + +## Scale & operations + +### Sessions and persistence + +Every conversation is a `session_id` GÇö a string the caller chooses. The full document state, conversation history, attachments, pending HITL changes, and AI working memory persist across calls and across server restarts. Reload an old session days later and the AI still has the full context. + +**Best for:** Long-running document workflows, async editing where the user comes back hours later, multi-turn editing where state carries between turns. + +### Async jobs with HITL state + +Long-running edits and HITL workflows return a `job_id`; the client polls or subscribes to SSE updates. State persists in a database, so any backend instance can pick up a job mid-flight (autoscaling, restarts, redeploys all safe). Approved changes resume automatically. + +**Best for:** Any workflow that takes more than 30 seconds or needs human approval mid-flight. + +### Per-organization feature flags + +B2B deployments can toggle specific features on or off per organization. Offer one platform integration to all customers but let Enterprise org A use a custom branding skin while Startup org B sticks with the default. Different orgs can have different rate limits, feature sets, or experimental rollouts GÇö all from the same codebase, all controlled by API. Useful for staged feature rollouts, customer-specific customization, and enterprise tier differentiation. + +**Best for:** Multi-tenant B2B platforms serving different customer tiers, gradual feature rollouts to specific orgs, enterprise customization without per-customer deployments. + +### Promo codes and credit allowances + +Issue promo codes that grant temporary operation allowances ("LAUNCH50" = 50 ops valid for 30 days, max 200 redemptions). Users redeem in Settings. Promo operations deplete before paid-tier operations so users get the most out of their allowance. Every redemption is tracked and auditable. Useful for go-to-market campaigns, partner enablement, customer pilots, and time-limited free trial extensions. + +**Best for:** Product launches, partner programs, customer pilots, trials, conferences, hackathons. + +### Multi-language editing + +Natural-language instructions and document content both work across many languages GÇö production users have edited documents in English, Spanish, French, Hebrew, Korean, Mandarin, and others (16+ languages confirmed in real usage so far). Write your prompt in one language, edit a document in another, get the AI's reply in whichever language you wrote the request. Multilingual documents (e.g., bilingual contracts) handled correctly. Tone and formality conventions adapted per language. + +**Best for:** International teams, multilingual document workflows, organizations serving non-English markets, contract translation and adaptation, cross-border legal work. + +### Build vertical AI on SuperDocs + +Combine attachments (your domain knowledge), sessions (long-running workflows), and chat instructions to build domain-specific applications on top of SuperDocs. **Contract AI**: attach your standard clause library + draft instructions, get an AI that writes contracts in your house style. **Compliance AI**: attach your regulations + policy templates, get an AI that audits documents for compliance gaps. **Marketing AI**: attach your brand voice guides + past collateral, get an AI that produces on-brand content. Same platform, different domain GÇö all configurable per session or per organization. + +**Best for:** Vertical SaaS platforms, agencies serving specific industries, organizations with strong domain languages, anyone building specialized document workflows. + +*** + +### Per-message chat revert + +Rewind a chat session to before any specific user message GÇö both the conversation **and** the document state snap back together. The reverted message text is returned so your UI can pre-fill a compose box for editing. The original branch is kept on the server for audit; the new branch becomes the active timeline. + +Available three ways: + +* **Web app**: a "Gå¦ Revert" button under every user message bubble, with a confirmation dialog before the rewind. +* **REST**: `POST /v1/sessions/{session_id}/revert` with `{turn_index}` GÇö see [Sessions](/concepts/sessions#revert-a-session-to-a-previous-message). +* **MCP**: the `revert_session_to_message` tool, callable by Claude Code, Cursor, Claude Desktop, and any MCP-compatible agent. + +If a chat job is currently running for the session, revert returns `409` GÇö wait for it to settle. Available on chats started after the feature shipped (older sessions don't carry the marker the rewind needs). + +**Best for:** "oops, undo that AI change" moments, exploring an alternate prompt without starting from scratch, recovering from a misunderstood instruction without losing the rest of your work. + +*** + +## On the roadmap + +### Branch switcher + +Today the original branch is preserved on the server side after a revert, but it isn't visible in the UI GÇö your active timeline is always the new branch. A future update will surface a switcher so you can navigate between alternate conversation paths the way ChatGPT and Claude do. + +*** + +## What ships when + +A live timeline of major capabilities and when they shipped. Older capabilities don't get less reliable over time GÇö once shipped, they stay covered by the regression suite and the production monitoring stack. + +| Date | Capability | Why it matters | +| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 2026-07-20 | **LaTeX upload** GÇö `.tex` files and whole LaTeX project `.zip` archives (root document + `\input` chapters + figures + `.bib` bibliography + custom class files; the shape LaTeX editors export) import as editable documents. The root file is detected automatically; math is **lossless** (every equation keeps its LaTeX source and exports as native Word math), footnotes become real footnotes, citations render against the bibliography, cross-references resolve to their numbers, tables (including merged cells and long tables) import as real tables, and the source's declared page size, margins, font size, and line spacing carry into the editor. Heavily visual custom-class documents GÇö many r+¬sum+¬ and poster templates GÇö are **compiled and imported with their visual layout preserved** instead of losing their content to an approximate text read. Anything unrepresentable (EPS figures, TikZ drawings, compile-time-only header macros) gets a visible placeholder plus a structured ingest warning; a genuinely broken source returns the compile error itself. | Papers, theses, r+¬sum+¬s, and math-heavy documents move from LaTeX into an editable, exportable document without retyping GÇö and nothing degrades silently: every fallback is visible and every failure message says what actually happened. | +| 2026-07-17 | **Round-trip fidelity and honesty batch**: on `.docx` export, equations (LaTeX) become **native Word math**, editable in Word rather than exported as pictures or plain text; a construct that can't be fully converted keeps its LaTeX source inside the equation and surfaces a `math_partial_fidelity` export warning. **Bookmarks and internal cross-reference targets** now survive upload, editing, and export, and **external hyperlinks** in uploaded `.docx` files stay live links. Word lists that take their numbering from a linked style import as real lists instead of plain paragraphs, and RTF uploads containing emoji or other unusual character escapes import cleanly. **Honest no-op replies**: an edit request the document already satisfies gets an honest "already satisfied" answer, changes nothing, and bills 0; a turn where every attempted operation failed reports the failure instead of claiming success. **Review-state recovery**: `GET /v1/sessions/{session_id}/jobs` (the `get_session_jobs` tool) reliably lists a session's jobs, and an `awaiting_approval` job's `metadata` carries `pending_batch_decisions` alongside `pending_changes`, so a client can rebuild its review UI after a reload without losing partially-decided batches. | Equations, bookmarks, cross-references, hyperlinks, and styled lists no longer degrade silently on the round-trip; agents and integrations can trust a "done" reply (no false success, and no charge when nothing needed changing); pending reviews survive page reloads and client restarts. | +| 2026-07-16 | **Document-fidelity upgrade** GÇö headers, footers, footnotes, endnotes, and comments become first-class parts you can **edit by chat**, proposed and approved through the same review flow as body edits (each part edit is its own before/after entry); **high-fidelity export** reproduces real Word footnotes/comments/headers and per-section page geometry (margins, orientation, section breaks, columns) on `.docx` and margin-box headers + typeset math on PDF, with a `fidelity` export option (`strict` default / `compat`); **legacy `.doc` / `.odt` / `.rtf` upload** normalized into the same rich pipeline as `.docx`; **scanned and complex-layout PDFs** read via text recognition and layout analysis; Google-Docs / Word-online paste preserves its formatting. | Formatting survives the full round-trip GÇö upload, edit, revert, multi-document, export GÇö with nothing flattened or dropped. Footnotes and comments stay real on export; page layout is preserved section by section; and headers/footers/notes are now editable by natural-language instruction. Existing integrations are unaffected GÇö parts editing rides the existing `chat` / `chat_async` tools and HITL flow, with no new tools, no new stream events, and no new required parameters. | +| 2026-06-25 | **Clone-and-update** GÇö ask the AI to recreate an existing document with new data ("duplicate this invoice for a new client, change the amounts to GǪ") and it reproduces the original's exact layout, fonts, columns, and structure, changing only the values you name. **Reliable large-document generation** GÇö generating a long, content-heavy document from scratch in a single request GÇö for example a multi-hundred-row table or a long structured report GÇö now completes in full. | Reproducing a document with new data preserves its formatting exactly instead of re-creating it from scratch; long generated documents come back whole. Both work through the existing `chat` / `chat_async` tools GÇö no new parameters, so existing integrations need no changes. | +| 2026-06-23 | **Durable Files, cross-session memory & search, concurrent-edit conflict resolution, and new MCP tools (35 total)** GÇö documents now persist across sessions as durable **Files**: `list_documents`, `get_document_detail`, `rename_document`, and `archive_document` (soft-delete, recoverable, purged after \~30 days) manage them, while `open_documents` loads saved files into a session and `init_session` creates a session and opens files in one call. Opt-in **cross-session memory and search** let a session draw on durable preferences and prior documents/chats GÇö four off-by-default `/v1/chat` + `/v1/chat/async` fields (`cross_session_memory`, `cross_session_search`, `cross_session_memory_key` for per-end-customer scoping, `cross_session_scope` to narrow which sessions are searched), with `clear_cross_session_memory` to wipe the saved note. A large edit that pauses can be resumed with the new `continue_chat` tool. Two more agent tools round out the set: `redo_revert` (undo a revert GÇö restore the session forward to its pre-revert state) and `unarchive_document` (restore an archived document). A new REST endpoint, `POST /v1/sessions/{session_id}/chunks/{chunk_id}/re-edit`, gives integrators explicit per-section conflict resolution (re-apply the AI's change over the user's current text, or blend the two) GÇö a billable AI edit, REST-only, not an MCP tool. All of this brings the MCP server to **35 tools**; all of cross-session is owner-scoped and never crosses the API-key owner. | Integrations get a persistent document library, optional long-term memory that an agent controls explicitly, deterministic resume for very large edits, undo-the-undo and restore-archived controls, deliberate per-section conflict handling for custom editors, and clean B2B2C isolation GÇö all opt-in, so existing integrations behave exactly as before. | +| 2026-06-11 | **Multi-document sessions** GÇö a session now holds multiple open documents: `open_mode` on upload (`replace`/`new_focused`/`background`) with the document roster in the response, three new endpoints + MCP tools to list/focus/close session documents, optional `document_id` targeting on `/v1/chat` + `/v1/chat/async`, cross-document reads/search/edits in one turn, and AI-created new documents from chat. **SSE additions** GÇö `documents_changed` (multi-doc auto-apply badge signal) and `model_fallback` (automatic Pro-tier failover notice) events; every event now carries `sequence` and reconnects can pass `last_sequence` to replay only newer events. **Thinking depth on every tier** GÇö `thinking_depth` (`fast`/`balanced`/`deep`) is now honored on all four model tiers. **Page geometry** GÇö document payloads include a nullable `page_setup` (size/orientation/margins detected from DOCX/PDF). **Billing clarity** GÇö every request bills one operation; very large requests bill one operation per 25 sections edited; denied review-mode changes are never billed. | One conversation can orchestrate several files the way a person with multiple tabs would; integrations get deterministic per-document targeting and review grouping; streams resume cleanly after disconnects; reasoning control is uniform across tiers; true-size page rendering becomes possible; operation counting matches intuition. | +| 2026-05-27 | Export pipeline overhaul GÇö multi-format export (`docx` default, `pdf`, `html`, `markdown`, `txt`) with per-request `options` (paper size, orientation, margins, filename, image embedding, PDF watermark + opacity), structured `X-Export-Warnings` header for non-fatal issues, three-tier flow for large documents (direct POST up to 20 MB GåÆ pre-signed upload for 20-100 MB via `upload_id` GåÆ email fallback for >100 MB via `POST /v1/documents/export/email-request`), structured 413 detail body with `error_code` and `suggested_action`, and full round-trip of Word track-changes on `.docx` export. | Integrators get five output formats from one endpoint, predictable size handling without a 413 surprise, and clear error contracts. The default-format flip from `doc` to `docx` is a soft compatibility change GÇö callers that omit `format` and switch on `Content-Type` now receive Open XML. | +| 2026-05-26 | Batched HITL approvals GÇö a turn's proposed changes arrive in a single `proposed_change_batch` SSE event (with a `changes[]` array) rather than fanning out N individual events. This is the canonical approval event and is emitted even when a turn has a single change. | Editors approving large changesets render one approval card instead of N. Wire load scales with turns, not changes. **Listen for `proposed_change_batch`** to receive every approval, single or batched. | +| 2026-05-25 | 30-minute wall-clock cap on chat turns (up from 10 minutes), parallel-edit speedup, latency improvements for very large documents. | Hundred-section edits in a single turn now finish inside the cap instead of timing out. Same 504 contract; the failure copy points users to splitting the work across smaller turns. | +| 2026-05-17 | Context-aware AI inserts GÇö when you ask the AI to insert an image, diagram, or new section without naming a position ("insert an image of a circle", "add a flowchart showing X", "add a new section here"), it now lands right after the section the user's cursor was in instead of always appending at end-of-document. Send the new optional `cursor_context` field on `/v1/chat` and `/v1/chat/async` to enable this; omit it and prior end-of-doc behaviour is preserved. The web app at use.superdocs.app sends it automatically. **Vague-verb clarification** GÇö broad ambiguous requests like "improve this document" or "make this better" now trigger a clarifying question with concrete options instead of an unexpected wide rewrite. | Integrators get a single-param opt-in to a noticeable UX improvement; existing integrations behave exactly as before if they omit the field. Web-app users see new content land near where they were looking. Vague broad-edit prompts no longer produce surprise rewrites GÇö the AI checks intent first. | +| 2026-05-16 | Multilingual + multi-cohort reliability GÇö the AI reasons about user intent across all languages (not enumerated phrase lists); RTL emails render correctly in Hebrew / Arabic / Persian / Urdu / etc.; per-cohort response tone (fresh vs returning vs committed users); style-attribute search ("change all the blue writing") matches inline HTML | Non-English and RTL-language users get parity quality; first-touch users see encouragement-shaped responses, returning users see direct-and-literal responses; visual-style queries resolve correctly across any HTML structure; safety-filtered AI responses degrade gracefully instead of crashing | +| 2026-05-06 | High-fidelity ingest GÇö PDF annotations + embedded images + Word track-changes preserved on import; reliable multi-file attachment-by-filename; document length awareness ("how many pages?") | PDFs, annotated review documents, and Word redlines no longer get silently flattened on upload; multi-file workflows resolve files by name; users get real page-count answers | +| 2026-05-06 | Web app first-paint upload affordances GÇö empty editor renders a drop zone for drag-drop, click-to-pick file picker, and a paste-as-content prompt when long content is pasted into a blank editor; "Starter Templates" relabel on the templates surface so it's clearly distinct from per-session attachments | First-time visitors find the upload path in seconds without asking; users who paste content as their first action get explicit "load as document content vs. treat as a chat instruction" choice | +| 2026-04-30 | Visual content & media GÇö image generate / edit / replace by chat, auto-rendered Mermaid diagrams, in-editor drawing canvas, KaTeX equations, auto table of contents | Documents are no longer text-only; images, diagrams, drawings, and equations live in the same chat-driven editing flow as the rest of the content | +| 2026-04-29 | Per-message chat revert (web app + REST + MCP `revert_session_to_message`) | "Oops, undo that" rewinds chat and document together; original branch preserved server-side | +| 2026-04-25 | Expanded features documentation (this page) GÇö 30+ capabilities grouped by use case | Developers and AI agents form a complete mental model of what SuperDocs can do | +| 2026-04-25 | MCP server unified GÇö 21 tools + 4 user-invocable workflow prompts on a single `/mcp` endpoint | Single MCP config entry covers both; discoverable slash commands for Cursor/Claude Code/Claude Desktop users | +| 2026-04-25 | Pre-signed URL upload/download flow (`request_upload_url`, `process_uploaded_document`, `request_download_url`) | Bytes no longer pass through agent context window; viable for real-world file sizes | +| 2026-04-25 | Compact response mode (`response_mode='compact'` + `chunk_diffs`) | \~140+ù token reduction for editing sessions on large documents | +| 2026-04-25 | Capability-forward MCP tool descriptions across all 21 tools | Agents form correct mental models of when to use SuperDocs vs build from scratch | +| 2026-04-23 | MCP HTTP transport reliability fix | Eliminates 5-minute hang clients (Claude Code, Cursor, Bun) saw on first connect | +| 2026-04-22 | OAuth Protected Resource Metadata (RFC 9728) for MCP | Cursor 3.x / Claude Code 2.x / mcp-remote can now connect without 60s metadata-probe timeout | +| 2026-04-19 | Editing latency improvements for large documents | 4m55s GåÆ \~10s for typical edit operations on large documents | +| 2026-04-18 | Editing precision improvements for nuanced instructions | Eliminates over-broad edits when the user's instruction was narrowly-scoped | +| Earlier 2026 | Async jobs + HITL durable state, SSE streaming, multimodal vision, multi-format export, MCP server, promo codes, billing | Foundation | + +*** + +For schemas and parameters, open the **API Reference** tab in the top navigation (auto-generated from the OpenAPI spec). For code examples see [cURL](/examples/curl), [Python](/examples/python), and [JavaScript](/examples/javascript); for the MCP tools see the [MCP Tools Reference](/mcp/available-tools). + + +# Agent Editing Playbook +Source: https://docs.superdocs.app/guides/agent-editing-playbook + +How an AI agent edits documents reliably on SuperDocs: choosing the right operation, section rewrites, cheap verification, placement, page breaks, and budgeting operations. + +# Agent Editing Playbook + +This is the field guide for AI agents (and the developers wiring them) doing document work over the REST API or MCP. Everything here applies to both surfaces GÇö MCP tool names in parentheses. + + + **The one-sentence version:** say what you want changed the way you'd tell a colleague GÇö "add a Benefits section after Compensation", "rewrite Section 6", "move the cover image to the top" GÇö and verify with the free `structure` read. You don't need to micro-manage. + + +## Choosing the operation (say what you mean) + +The AI picks the operation from your message. Three intents cover editing: + +| Your intent | Say it like | What happens | +| ------------------------------ | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| **Add** new content | "Add an Appendix A withGǪ", "Insert a section on X after Y" | A new section is inserted at the position you named. The rest of the document is untouched. | +| **Change** existing content | "Rewrite Section 6 toGǪ", "Fix the dates in the payment clause", "Translate the introduction" | The targeted content is edited in place. | +| **Rebuild** the whole document | "Replace the whole document withGǪ", "Regenerate this from scratch asGǪ" | The entire document is replaced. Only happens when you explicitly ask for a full replacement. | + +Two behaviors worth knowing: + +* **Section rewrites are atomic.** "Rewrite Section 6" replaces the whole section GÇö heading to next heading GÇö as one unit. New content in, all old section content out. You will not get a half-merged section with stale paragraphs left behind. +* **Find-and-modify is one request.** "Find the line that says X and replace it with Y" performs the edit. You don't need a separate search call first. + +## Verify for free (never export just to check) + +`GET /v1/documents/{id}` (`get_document_detail`) always includes a `structure` block GÇö headings with levels and positions, `section_count`, `block_count`, and media counts GÇö derived on read, **never billed**. When the document carries page headers, footers, footnotes, endnotes, or comments, `structure` also includes a `parts` list naming each one with its type, a short preview, and the `chunk_id` you can reference when asking for a change. Counts and heading positions describe the document body; parts are listed separately and never inflate `section_count`. + +```json theme={null} +"structure": { + "headings": [{"level": 1, "text": "Employee Handbook", "position": 0}, + {"level": 2, "text": "Section 1 GÇö Hours", "position": 2}], + "section_count": 18, + "block_count": 74, + "media": {"images": 3, "diagrams": 1} +} +``` + +Check it after any edit you care about. Exports are for deliverables, not verification. + +## Keep your token footprint small + +For large documents, don't pull the full document body into your agent's context to work on it. The document lives server-side; targeted natural-language requests ("rewrite Section 6") edit it there, and the `structure` block above verifies the result without fetching a single body byte. Set `response_mode='compact'` on `chat`/`chat_async` so responses carry per-section diffs instead of the full HTML, and move file bytes with the pre-signed upload/download URL flows so they never enter your context either. A 100-page document can go through a full multi-turn editing session while your agent's context holds only a few thousand tokens. + +## Placement (images, diagrams, new sections) + +Name the position and it is honored: "at the top", "in Section 2", "before the conclusion", "replace the \[COVER] placeholder". If a named position can't be resolved, the operation fails honestly and the AI retries with the real location GÇö content is never silently dumped at the end of the document when you named a spot. + +To reposition existing content, ask for a move: "move the org chart into Section 2", "move the banner to the top". Moves relocate the original GÇö nothing is duplicated or regenerated. + +## Headers, footers, footnotes, and comments + +Out-of-flow parts are edited the same way as body content: "change the footer to include the confidentiality notice", "fix the typo in footnote 3", "add a comment on the payment clause". Part edits ride the same request, the same review flow, and the same billing as body edits; they never change the body `section_count`. On `.docx` export an edited footnote is still a real Word footnote, a comment a real Word comment. The free `structure` read lists every part with its id under `parts` (see above), so you can verify a part edit without exporting. + +## Page breaks and tables of contents + +* **Page breaks:** "start each section on a new page" / "page break before the appendix" inserts real page-break markers. DOCX exports carry true Word page breaks; PDF exports break pages there. +* **Table of contents:** "add a table of contents" inserts a live TOC that updates itself from your headings GÇö in the editor and in every export. One request; the document structure is otherwise untouched. + +## Long generations: use async + +Synchronous `POST /v1/chat` is right for edits and normal-sized creations. For **document-scale generations** GÇö a whole handbook, a long report GÇö use `POST /v1/chat/async` (`chat_async`): it returns a `job_id` immediately and reports progress via `GET /v1/jobs/{job_id}` (`get_job`). Sync requests that run very long can hit the platform gateway timeout (\~300s) and return nothing. Long sync responses also carry an advisory `hint` telling you when to switch. + +## Operations and billing (what a request costs) + +* Most requests bill **1 operation**. Very large creations bill **1 per 25 sections** created (a 96-section handbook Gëê 4 operations). +* **Failed operations bill 0.** If the AI couldn't complete your edit, the reply says so and you were not charged. +* **Already-satisfied requests bill 0.** If the document already meets your instruction, the reply says so honestly; nothing is changed and nothing is charged. Don't re-issue the request expecting a rewrite. +* Reads are free or flat: `structure`, listing documents/sessions, job polling GÇö none of it eats your document-edit budget. Searches within your documents bill 1. +* Check remaining quota anytime: `get_account_status` / `GET /v1/agents/whoami`. + +## Session and Files ids + +`GET /v1/sessions/{id}/documents` (`list_session_documents`) returns both a session-local `document_id` and a permanent `durable_document_id` GÇö the same id `list_documents` (Files), `get_document_detail`, `rename_document`, and `archive_document` use. **Everything that takes a session document id also accepts the durable UUID**, so you can drive a whole workflow with one id form. + +Two honesty guarantees while cleaning up: + +* Archiving a document that's open in another session fails with **409 `document_in_use`** (nothing archived) GÇö re-call with `force=true` or close it there first. +* Reverting to a turn that a previous revert already removed fails with **409 `turn_already_reverted`** and includes the current `active_turns` map so you can re-target immediately. + +## Credentials and handoff + +* **Before signing up, check `~/.superdocs/agent_credentials.json`.** If it exists, call `GET /v1/agents/whoami` with that key and reuse the account GÇö its documents and quota carry over. +* Nearing the cap: `POST /v1/agents/handoff` with your human's email. Pass `working_context` (what you're working on, where you run) so the email is recognizable, and relay the returned **takeover code** to your operator in chat GÇö never by email. They open the link, enter the code, and adopt the account in place; you keep the same key and all your work. + + +# Agent Tool Integration +Source: https://docs.superdocs.app/guides/agent-tool-integration + +Register SuperDocs as a tool in your existing AI agent's tool registry GÇö OpenAI function-calling, Anthropic tool_use, LangChain, LlamaIndex, or any function-calling-compatible framework. Your AI is the consumer; SuperDocs is the document-editing capability it can call. + +# Agent Tool Integration + +**Quick path:** new to the API? Go from an API key to your first successful call in the [Quickstart](/introduction/quickstart) (\~5 minutes), then come back here to register SuperDocs as an agent tool. + +If your product has an existing AI agent (built with OpenAI function-calling, Anthropic `tool_use`, LangChain, LlamaIndex, or a custom framework) that needs to edit documents, the right integration shape is to register SuperDocs as a tool the agent can call GÇö not to build a separate UI around SuperDocs. + + + **Your agent can pick `model_tier` per call based on user intent.** A simple "fix typo" call can use `core`; a contract-clause edit can switch to `max` mid-conversation. Add `model_tier` and `thinking_depth` to your tool's parameter schema and let the agent reason about which tier fits the current task. See [Model Selection](/guides/model-selection) for the full matrix. + + +This guide covers the pattern + working snippets for the five most common agent frameworks, plus a generic JSON-schema definition you can adapt to any framework that supports tool-calling. + +## Why this shape is different from a UI integration + +In a typical SuperDocs UI integration, a human types in a chat panel, the chat sends a message to SuperDocs, and SuperDocs returns proposed edits the human reviews. The customer-facing app is the consumer of SuperDocs. + +In an agent-tool integration, your AI agent is the consumer. The agent decides GÇö based on its own reasoning, the user's query, and the conversation context GÇö that it needs to edit a document. It invokes SuperDocs as a tool, receives proposed changes as structured data, and either approves them itself (auto-decide), surfaces them through your existing UI for the user to review, or sends them out-of-band (Slack, email). + +The integration is server-side, the SuperDocs `sk_` key lives next to your other model API keys, and there is no SuperDocs-specific UI to build GÇö your agent's existing UI is where the result appears. + +## The pattern, in 4 steps + +1. **Define the SuperDocs tool** in your agent's tool registry. The two required parameters are `message` (the natural-language instruction for the AI) and `session_id` (a string that ties multiple turns together). `document_html` (the HTML the agent wants edited) is optional GÇö send it on the first turn, then omit it: the server persists document state by `session_id` across turns, so resending the full HTML every turn just wastes tokens. +2. **Implement the tool function.** Call `POST /v1/chat` with the three parameters plus your chosen `approval_mode`. Return the response back to the agent GÇö either as the parsed proposed changes (for `ask_every_time`) or as the final updated HTML (for `approve_all`). +3. **Decide approval shape.** Choose between `approve_all` (your agent's reasoning is the approval GÇö fastest), `ask_every_time` (the agent reviews each proposed change individually before deciding), or surface to a human through your UI. +4. **Persist the result.** The agent receives the updated HTML and writes it back to wherever your documents live. + +That's the whole loop. The snippets below are the same loop in five different framework idioms. + +## Generic JSON-schema tool definition + +Every framework that supports tool-calling accepts a JSON-schema tool definition. Use this as the canonical version and adapt for your framework: + +```json theme={null} +{ + "name": "edit_document", + "description": "Edit a document with AI by sending the document HTML and a natural-language instruction. Returns the updated HTML. Use this when the user asks to modify, rewrite, expand, summarize, translate, or improve any document.", + "parameters": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "The natural-language instruction for what to do to the document. Examples: 'rewrite the introduction in plain English', 'add a section about pricing', 'tighten the conclusion to one paragraph', 'translate to French'." + }, + "session_id": { + "type": "string", + "description": "A string that ties multiple turns together. Use the same session_id across multiple tool calls if you want the AI to remember earlier edits in the same conversation." + }, + "document_html": { + "type": "string", + "description": "The current HTML of the document to be edited, as a complete HTML string. OMIT this once the session already holds the document GÇö the server persists document state across turns by `session_id`, so resending the full HTML every turn just burns tokens. Send it on the first turn (or when you're switching the session to a new document). The response includes `data-chunk-id` attributes GÇö preserve them on the next call so SuperDocs can target previously-edited sections precisely." + }, + "approval_mode": { + "type": "string", + "enum": ["approve_all", "ask_every_time"], + "description": "Whether to apply all proposed changes automatically (approve_all GÇö best for autonomous agents) or pause for review on each change (ask_every_time GÇö best when a human is in the loop)." + } + }, + "required": ["message", "session_id"] + } +} +``` + +## OpenAI function-calling (Python) + +```python theme={null} +import os, httpx, json +from openai import OpenAI + +client = OpenAI() +SUPERDOCS_KEY = os.environ["SUPERDOCS_API_KEY"] + +# 1. Tool definition GÇö registered when you call the model +edit_document_tool = { + "type": "function", + "function": { + "name": "edit_document", + "description": "Edit a document with AI. Returns the updated HTML.", + "parameters": { + "type": "object", + "properties": { + "message": {"type": "string", "description": "The instruction for what to do to the document."}, + "session_id": {"type": "string", "description": "Ties multi-turn edits together."}, + "document_html": {"type": "string", "description": "Current HTML to edit. Omit once the session already holds the document GÇö state persists by session_id."}, + "approval_mode": {"type": "string", "enum": ["approve_all", "ask_every_time"]}, + }, + "required": ["message", "session_id"], + }, + }, +} + +# 2. Tool function GÇö called when the model invokes the tool +def edit_document(message: str, session_id: str, document_html: str, approval_mode: str = "approve_all") -> dict: + with httpx.Client(headers={"Authorization": f"Bearer {SUPERDOCS_KEY}"}, timeout=300) as c: + r = c.post("https://api.superdocs.app/v1/chat", json={ + "message": message, + "session_id": session_id, + "document_html": document_html, + "approval_mode": approval_mode, + }) + r.raise_for_status() + result = r.json() + return { + "updated_html": result["document_changes"]["updated_html"], + "ai_response": result.get("response", ""), + "usage": result.get("usage", {}), + } + +# 3. Standard agent loop GÇö the model decides when to call edit_document +def run_agent(user_message: str, document_html: str, session_id: str): + messages = [ + {"role": "system", "content": "You are an SOP-editor assistant. When the user asks to modify a document, use the edit_document tool."}, + {"role": "user", "content": f"{user_message}\n\nCurrent document HTML available GÇö call edit_document with session_id={session_id}."}, + ] + response = client.chat.completions.create( + model="gpt-5", # Your model choice GÇö SuperDocs is provider-agnostic + messages=messages, + tools=[edit_document_tool], + ) + msg = response.choices[0].message + if msg.tool_calls: + for call in msg.tool_calls: + args = json.loads(call.function.arguments) + args["document_html"] = args.get("document_html") or document_html + result = edit_document(**args) + # Append result back to the conversation if you want a multi-turn flow + messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)}) + return result + return {"ai_response": msg.content} +``` + +## Anthropic `tool_use` (Python) + +```python theme={null} +import os, httpx, anthropic + +client = anthropic.Anthropic() +SUPERDOCS_KEY = os.environ["SUPERDOCS_API_KEY"] + +edit_document_tool = { + "name": "edit_document", + "description": "Edit a document with AI. Returns the updated HTML.", + "input_schema": { + "type": "object", + "properties": { + "message": {"type": "string"}, + "session_id": {"type": "string"}, + "document_html": {"type": "string", "description": "Omit once the session already holds the document GÇö state persists by session_id."}, + "approval_mode": {"type": "string", "enum": ["approve_all", "ask_every_time"]}, + }, + "required": ["message", "session_id"], + }, +} + +def edit_document(message, session_id, document_html, approval_mode="approve_all"): + with httpx.Client(headers={"Authorization": f"Bearer {SUPERDOCS_KEY}"}, timeout=300) as c: + r = c.post("https://api.superdocs.app/v1/chat", json={ + "message": message, "session_id": session_id, + "document_html": document_html, "approval_mode": approval_mode, + }) + r.raise_for_status() + return r.json() + +def run_agent(user_message, document_html, session_id): + messages = [{"role": "user", "content": f"{user_message} (session_id={session_id})\n\nDocument HTML: {document_html[:500]}..."}] + response = client.messages.create( + model="claude-opus-4-5", # Your model choice + max_tokens=4096, + system="You are an SOP-editor assistant. Call edit_document when the user asks to modify a document.", + tools=[edit_document_tool], + messages=messages, + ) + for block in response.content: + if block.type == "tool_use" and block.name == "edit_document": + args = block.input + args["document_html"] = args.get("document_html") or document_html + result = edit_document(**args) + return result["document_changes"]["updated_html"] + return None +``` + +## LangChain `Tool` (Python) + +```python theme={null} +import os, httpx +from langchain.tools import tool +from langchain.agents import create_react_agent +from langchain_openai import ChatOpenAI + +SUPERDOCS_KEY = os.environ["SUPERDOCS_API_KEY"] + +@tool +def edit_document(message: str, session_id: str, document_html: str, approval_mode: str = "approve_all") -> str: + """Edit a document with AI. Returns the updated HTML. + + Args: + message: The natural-language instruction for what to do to the document. + session_id: A string that ties multiple turns together. + document_html: The current HTML of the document to be edited. + approval_mode: 'approve_all' to auto-apply, 'ask_every_time' to pause per change. + """ + with httpx.Client(headers={"Authorization": f"Bearer {SUPERDOCS_KEY}"}, timeout=300) as c: + r = c.post("https://api.superdocs.app/v1/chat", json={ + "message": message, "session_id": session_id, + "document_html": document_html, "approval_mode": approval_mode, + }) + r.raise_for_status() + return r.json()["document_changes"]["updated_html"] + +# Wire into your agent +llm = ChatOpenAI(model="gpt-5") +tools = [edit_document] # Plus whatever other tools your agent has +agent = create_react_agent(llm, tools, prompt="...") +``` + +## LlamaIndex `FunctionTool` (Python) + +```python theme={null} +import os, httpx +from llama_index.core.tools import FunctionTool +from llama_index.core.agent import ReActAgent +from llama_index.llms.openai import OpenAI + +SUPERDOCS_KEY = os.environ["SUPERDOCS_API_KEY"] + +def edit_document(message: str, session_id: str, document_html: str, approval_mode: str = "approve_all") -> str: + """Edit a document with AI. Returns the updated HTML.""" + with httpx.Client(headers={"Authorization": f"Bearer {SUPERDOCS_KEY}"}, timeout=300) as c: + r = c.post("https://api.superdocs.app/v1/chat", json={ + "message": message, "session_id": session_id, + "document_html": document_html, "approval_mode": approval_mode, + }) + r.raise_for_status() + return r.json()["document_changes"]["updated_html"] + +edit_tool = FunctionTool.from_defaults(fn=edit_document, name="edit_document", + description="Edit a document with AI. Returns updated HTML.") + +agent = ReActAgent.from_tools([edit_tool], llm=OpenAI(model="gpt-5"), verbose=True) +``` + +## TypeScript / Vercel AI SDK + +```typescript theme={null} +import { tool } from "ai"; +import { z } from "zod"; + +const SUPERDOCS_KEY = process.env.SUPERDOCS_API_KEY!; + +export const editDocument = tool({ + description: "Edit a document with AI. Returns the updated HTML.", + parameters: z.object({ + message: z.string().describe("The instruction for what to do to the document."), + session_id: z.string().describe("Ties multi-turn edits together."), + document_html: z.string().describe("Current HTML to edit."), + approval_mode: z.enum(["approve_all", "ask_every_time"]).default("approve_all"), + }), + execute: async ({ message, session_id, document_html, approval_mode }) => { + const res = await fetch("https://api.superdocs.app/v1/chat", { + method: "POST", + headers: { + "Authorization": `Bearer ${SUPERDOCS_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ message, session_id, document_html, approval_mode }), + }); + if (!res.ok) throw new Error(`SuperDocs ${res.status}`); + const data = await res.json(); + return { + updated_html: data.document_changes.updated_html, + ai_response: data.response, + }; + }, +}); + +// Wire into your agent's tool registry alongside your other tools +``` + +## Approval modes GÇö choose based on who decides + +### `approve_all` GÇö your agent's reasoning is the approval + +The simplest pattern. Pass `approval_mode: "approve_all"` and SuperDocs returns the final updated HTML in one call. Use this when your agent is autonomous and you trust it to make the right call. Most appropriate for: + +* Backend AI agents processing a queue without human oversight. +* Agents whose user already approved the high-level intent (e.g., "rewrite all sections in plain English") and doesn't need per-change review. +* Server-to-server workflows. + +### `ask_every_time` GÇö your agent reviews each proposed change + +Pass `approval_mode: "ask_every_time"` and SuperDocs returns the turn's proposed changes via SSE. The whole turn arrives as one `proposed_change_batch` event whose `changes[]` array holds every proposed change (a single-change turn arrives as a one-element array). Your agent reads that event (parse `event.data` then `JSON.parse` the `content` field again GÇö see the [SSE Streaming guide](/guides/streaming) for the double-parse warning), loops over `changes[]` deciding whether to approve each based on its own reasoning, and POSTs to `/v1/chat/{session_id}/approve` to accept or reject. (You may also register a `proposed_change` listener defensively for forward-compatibility, but it never fires today GÇö build your review loop on `proposed_change_batch`.) + +Use this when: + +* Your agent has higher reasoning capability than SuperDocs' edit suggestions and wants to filter them. +* The user is reviewing the agent's overall plan but trusts the agent to filter individual edits. +* You want an audit log of every accepted vs rejected change for compliance. + +The decision flow inside the agent looks like: + +```python theme={null} +# Pseudocode GÇö adapt to your framework +# Each batch is the whole turn: parse the proposed_change_batch event, then +# loop over its changes[] array (one element for a single-change turn). +for batch in stream_proposed_change_batches(job_id, session_id): + decisions = [] + for change in batch["changes"]: + # The agent reasons about whether to approve each change + decision = await agent.reason( + f"SuperDocs proposed: {change['ai_explanation']}\n" + f"Old: {change['old_html']}\n" + f"New: {change['new_html']}\n" + "Approve or deny?" + ) + decisions.append({"change_id": change["change_id"], "approved": decision == "approve"}) + httpx.post( + f"https://api.superdocs.app/v1/chat/{session_id}/approve", + headers={"Authorization": f"Bearer {SUPERDOCS_KEY}"}, + json={"job_id": job_id, "approved": True, "changes": decisions}, + ) +``` + +### Surface to a human through your UI + +If your agent is in a chat with a human and SuperDocs proposes an edit, your agent can stop, surface the proposed change to the human via your existing chat thread (as a card, a button group, an inline question GÇö whatever your UI does), wait for the human's decision, and then POST the decision back to SuperDocs. + +This pattern keeps your AI agent in control of what gets surfaced and when, but defers the binary approve/deny decision to the human. The agent's UI is the only UI involved GÇö there's no SuperDocs-specific UI. + +## Document HTML round-trip GÇö same rule as everywhere else + +The `data-chunk-id` attributes that SuperDocs adds to block-level elements **must round-trip cleanly** when you send the HTML back on the next call. If your agent stores the HTML in a database, file, or in-memory variable between calls, those attributes must survive the storage cycle. Most string-based storage preserves them automatically; if you parse the HTML through a library that strips unknown attributes, you'll break the round-trip silently. + +If you're piping the HTML through a rich-text editor at any point in your agent's UI, see the [Editor Integration guide](/guides/editor-integration) for editor-specific preservation patterns. + +## Multi-document sessions for agents + +A session can hold several open documents. Your agent targets one explicitly by passing `document_id` on `chat`/`chat_async` (ids come from the `list_session_documents` tool / `GET /v1/sessions/{id}/documents`), names it in the message ("update the invoice"), or omits both to hit the focused document. Agents that juggle related files (a contract + its amendment, a report + its data appendix) should keep them in ONE session and let the AI read across them, instead of copying content between sessions. See the [Multi-Document guide](/guides/multi-document). + +## Letting your agent undo a step + +If your agent decides GÇö mid-workflow GÇö that a previous edit was wrong, it can rewind the session to before the user message that triggered the bad edit instead of trying to patch the result forward. Call `POST /v1/sessions/{session_id}/revert` with the `turn_index` of the user message you want to undo. SuperDocs returns the document state from before that turn plus the original message text, the agent can rewrite the instruction more clearly, and the session continues from the rewind point. The original conversation is preserved for audit but hidden from active reads. + +Concrete shape GÇö register revert as a second tool alongside `edit_document`: + +```python theme={null} +def revert_session(session_id: str, turn_index: int) -> dict: + with httpx.Client(headers={"Authorization": f"Bearer {SUPERDOCS_KEY}"}, timeout=30) as c: + r = c.post(f"https://api.superdocs.app/v1/sessions/{session_id}/revert", + json={"turn_index": turn_index}) + r.raise_for_status() + return r.json() # {compose_text, reverted_to_turn, document_state, editor_action, archived_turn_count} +``` + +The agent reasons "this edit went wrong, let me try a different prompt" and chains `revert_session` GåÆ `edit_document` with the corrected instruction. Returns `409` if a chat job is still in flight on the session GÇö wait for it to settle first. + +See the full revert contract in [Sessions GåÆ Revert a session to a previous message](/concepts/sessions#revert-a-session-to-a-previous-message). + +## Choosing between sync `/v1/chat` and async `/v1/chat/async` + +Most agent-tool integrations should use sync `/v1/chat`. It's a simpler integration (one call, one response) and matches the standard tool-call shape (function in, JSON out, agent reasons over the result). + +Use async `/v1/chat/async` + the SSE stream when: + +* Your agent has a UI that wants to show streaming progress events to the user as the AI works. +* The document is very large and you want to fail fast on auth errors before committing to a long edit. +* You want to use `ask_every_time` and react to each proposed change in real time. + +For everything else, sync is enough. + +## Stuck? + +If your agent framework isn't covered here or the tool-calling pattern doesn't map cleanly to your setup, email [hello@superdocs.app](mailto:hello@superdocs.app) or book a 15-minute integration call at [cal.com/superdocs](https://cal.com/superdocs). We'll talk through the pattern and add a snippet to this guide for the next person. + +## Related guides + +* [Server Integration](/guides/server-integration) GÇö if your agent is one of several services in a backend, the same patterns apply more broadly. +* [Human-in-the-Loop](/guides/human-in-the-loop) GÇö if your agent surfaces proposed changes to a human via your UI, the rendering patterns there work for chat-thread cards too. +* [SSE Streaming](/guides/streaming) GÇö if you choose `ask_every_time` and need to consume the turn's `proposed_change_batch` event. +* [Async Jobs](/guides/async-jobs) GÇö if you want polling instead of SSE. +* [Integration Starter Prompt](/guides/integration-starter-prompt) GÇö paste this into your coding agent to wire all the above up automatically. + + +# AI-to-AI Integration +Source: https://docs.superdocs.app/guides/ai-to-ai + +Connect your AI agent to SuperDocs for autonomous document editing without a browser. + +# AI-to-AI Integration + +Your AI agent can use SuperDocs as a document editing tool. No browser, no frontend GÇö just API calls. Your AI decides what to edit, SuperDocs executes. + +## How it works + + + + + + + + + + + +This creates a loop: your AI agent controls *what* to edit, SuperDocs handles *how* to edit it. + +## Example: Autonomous document workflow + +```python theme={null} +import requests + +API_KEY = "sk_YOUR_API_KEY" +HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"} +BASE = "https://api.superdocs.app/v1/chat" + +# Your AI agent's workflow +tasks = [ + "Create a project proposal with 4 sections: Overview, Goals, Timeline, Budget", + "Add specific milestones to the Timeline section", + "Add cost estimates to the Budget section", + "Review the entire document and fix any inconsistencies" +] + +session_id = "ai-agent-proposal" +document_html = "" + +for task in tasks: + response = requests.post(BASE, headers=HEADERS, json={ + "message": task, + "session_id": session_id, + "document_html": document_html + }) + + data = response.json() + print(f"Task: {task}") + print(f"AI: {data['response'][:100]}...") + + # Use the updated document for the next task + if data.get("document_changes"): + document_html = data["document_changes"]["updated_html"] + +print("\nFinal document ready.") +``` + +## Session persistence + +Use the same `session_id` across calls. SuperDocs remembers the full conversation, so your agent can reference previous changes: + +```python theme={null} +# First call +requests.post(BASE, headers=HEADERS, json={ + "message": "Draft a sales contract", + "session_id": "agent-contract", + "document_html": "" +}) + +# Later call GÇö SuperDocs remembers the contract it drafted +requests.post(BASE, headers=HEADERS, json={ + "message": "Add a payment terms section based on net-30", + "session_id": "agent-contract", + "document_html": current_html +}) +``` + +## Undoing a step + +If your agent realizes one of its earlier edits was wrong, it can rewind the session instead of trying to patch the result forward. Call `POST /v1/sessions/{session_id}/revert` with the `turn_index` of the user message you want to undo: + +```python theme={null} +revert = requests.post( + f"https://api.superdocs.app/v1/sessions/agent-contract/revert", + headers=HEADERS, + json={"turn_index": 4}, # the user message you want to undo +) +result = revert.json() +# result["compose_text"] GåÆ the original message text +# result["document_state"] GåÆ the document HTML at the rewind point +# result["reverted_to_turn"] GåÆ the turn the conversation now ends at +``` + +The agent receives the document state from before that turn plus the original message text, rewrites the instruction more carefully, and re-sends. The original conversation is preserved for audit but hidden from active reads. Returns `409` if a chat job is currently running on the same session GÇö wait for it to settle first. See [Revert a session to a previous message](/concepts/sessions#revert-a-session-to-a-previous-message). + +## With MCP + +If your AI agent supports MCP (like Claude), connect it directly to SuperDocs. See [MCP Setup](/mcp/setup) for setup. The AI agent gets native access to all 38 SuperDocs tools GÇö including `revert_session_to_message` GÇö without writing any API client code. + + +# Async Jobs +Source: https://docs.superdocs.app/guides/async-jobs + +Use async chat requests with job polling for long-running operations and background processing. + +# 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 GÇö the fastest tier, optimised for high-volume workflows. For high-stakes documents in your batch, use `pro` or `max` instead. See [Model Selection](/guides/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 + +```bash theme={null} +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: + +```json theme={null} +{ + "job_id": "550e8400-e29b-41d4-a716-446655440000", + "session_id": "my-session", + "status": "pending", + "message": "Chat request queued for processing" +} +``` + +The `job_id` is an opaque UUID GÇö treat it as a string, don't parse it. Chat jobs and attachment-processing jobs share the same id space and the same `GET /v1/jobs/{job_id}` path. + +### 2. Poll for status + +```bash theme={null} +curl https://api.superdocs.app/v1/jobs/550e8400-e29b-41d4-a716-446655440000 \ + -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: + +```json theme={null} +{ + "job_id": "550e8400-e29b-41d4-a716-446655440000", + "status": "completed", + "result": { + "response": "I've restructured the document into 5 sections...", + "session_id": "my-session", + "document_changes": { + "updated_html": "
...
", + "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 GÇö check `metadata.awaiting_kind` | **Change review** (HITL): read `metadata.pending_changes`, call `/approve`. **Continue prompt** (large edit): read `metadata.continue_prompt`, call `/continue` GÇö see [Continuing a large edit](#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](/concepts/sessions#revert-a-session-to-a-previous-message) 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"` GÇö 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: + +```json theme={null} +{ + "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 GÇö or stop GÇö by calling `/continue`: + +```bash theme={null} +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": "550e8400-e29b-41d4-a716-446655440000", "continue": true}' +``` + +* `continue: true` GÇö resume; the job returns to `in_progress` and finishes the rest. +* `continue: false` GÇö 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 GÇö 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 (3GÇô5 sections) | 30GÇô90 seconds | 2 minutes | 5 minutes | +| Full-document operation (10+ sections) | 60GÇô180 seconds | 3 minutes | 10 minutes | +| Complex operation (merge, restructure, batch) | 120GÇô300 seconds | 4 minutes | 15 minutes | +| Very large document (hundreds of sections) | 5GÇô25 minutes | 8 minutes | 30 minutes (server cap) | + +These are medians for typical documents (5GÇô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 GÇö see [Model Selection](/guides/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 + +```python theme={null} theme={null} +import time +import requests + +MAX_WAIT = 300 # 5 minutes GÇö 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 GÇö see [Streaming GåÆ Rendering intermediate events](/guides/streaming#rendering-intermediate-events-in-your-ui). + +### Job retention + +All jobs GÇö pending, in-progress, awaiting-approval, or terminal GÇö 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](/concepts/documents#email-fallback-for-very-large-documents). | + +`large_export` jobs are fire-and-forget GÇö 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: + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/jobs/550e8400-e29b-41d4-a716-446655440000/cancel \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +## List all jobs + +```bash theme={null} +curl https://api.superdocs.app/v1/jobs \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +## Polling example + +```python theme={null} +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 GÇö 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 GÇö the job returns to in_progress and may pause again + else: + # HITL change review GÇö 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 GÇö approve or deny changes within this window. + + + +# Concurrent & Cross-Session Editing +Source: https://docs.superdocs.app/guides/concurrent-editing + +Keep a document in sync when more than one place edits it at once GÇö two browser sessions, a UI tab plus a background job, or any writer-plus-poller pair. Poll for other sessions' edits, autosave human typing, and merge instead of clobbering. + +# Concurrent & Cross-Session Editing + +A single document can be open in more than one place at the same time: two browser tabs, a user editing in your UI while a background job rewrites the same file, or any integration that both writes and reads a document on separate connections. Without coordination, the last writer silently wins and someone's work disappears. + +SuperDocs gives you the pieces to keep those copies converged GÇö a lightweight **change feed** to learn when another session edited a document you hold, and a **human autosave** endpoint so pure typing is persisted and announced. This guide is the integrator pattern for wiring them together. + + + You only need this guide if the **same** document can be edited from two places concurrently. If each session owns its own documents, skip it GÇö there's nothing to reconcile. For the per-section apply discipline this builds on, see [Applying AI updates safely](/guides/editor-integration#applying-ai-updates-safely-without-losing-user-edits). + + +## The model + +* Every open document lives in a session and has a stable `document_id` and a committed version. +* When any session commits a change to a document (an AI turn, or a human autosave), other sessions that hold the **same** document can learn about it by polling a change feed. +* On a change, you re-fetch that one document and apply it **per section** onto your live editor GÇö never a whole-document reload, which would wipe whatever the user is typing right now. + + + **`window_id` (optional) GÇö disambiguate two windows of the *same* session.** `chat`, `chat_async`, `revert`, and `redo` accept an optional `window_id`. If you run two windows of the **same** `session_id` (rather than two separate sessions), set a stable per-window id so the server can tell concurrent writers apart and merge their edits instead of letting one overwrite the other. It's optional GÇö the per-section apply discipline in this guide works without it, and most integrators using one session per place don't need it GÇö but it's the precise lever for the same-session, multi-window case. + + +This is soft, eventually-consistent collaboration GÇö converge-on-poll, not a real-time cursor-sharing CRDT. It's deliberately simple: a REST poll on a 1GÇô2 second cadence, no held connection per editor. + +## Polling for other sessions' edits + +``` +GET /v1/sessions/{session_id}/doc-events?after_id={cursor} +``` + +This returns the change events for documents **this session holds** that **other** sessions committed since `after_id`. Poll it roughly every 1GÇô2 seconds while a document is open. + +* **`after_id`** is your cursor. Start at `0` (or omit it), then pass back the highest event `id` you've seen so each poll only returns what's new. +* **`include_own`** GÇö by default the feed **excludes this session's own writes** (you already have your own changes; no need to echo them back). A REST or MCP integrator that **writes on one connection and polls on another** GÇö or just wants a complete feed for every document it holds GÇö should pass `include_own=true`. Otherwise you'd never hear about edits your own background worker made on a different connection. + + + This feed is **REST-only by design**. It is intentionally not an MCP tool and not a held SSE connection GÇö it's a cheap stateless poll, which keeps long-lived connections free. (The SSE stream still carries *your own* turn's events like `documents_changed`; the doc-events feed is specifically for changes that originate in **other** sessions.) + + +```bash theme={null} +curl "https://api.superdocs.app/v1/sessions/my-session/doc-events?after_id=0" \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +### Event types + +Each event names a `document_id` and what happened to it: + +| Event | Meaning | What to do | +| ------------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| `document_updated` | Another session committed content changes to this document. | Re-fetch the document and apply its changed sections (below). | +| `document_removed` | Another session archived (soft-deleted) the document. | Drop the tab, or GÇö if the user is mid-edit in it GÇö offer a keep-editing choice (see [Restoring a removed document](#restoring-a-removed-document)). | +| `document_restored` | A previously archived document was brought back. | Re-add the tab and load its current content. | +| `document_renamed` | The document's title changed. | Update the tab label. No content fetch needed. | + +An event tells you *that* something changed, not the full new body GÇö re-fetch the one document it names rather than trusting any content in the event itself. + +## Applying a received change + +When you get a `document_updated` event: + +1. **Re-fetch that one document.** Read it from the roster with the body included GÇö `GET /v1/sessions/{session_id}/documents?include_html=true` GÇö or from session history. Fetch only the document the event named, not the whole roster's bodies. +2. **Apply per section GÇö never whole-replace.** Replace only the `data-chunk-id` blocks that actually differ from what you currently render, and leave every other block GÇö including the section the user is typing in *right now* GÇö untouched. Reloading the entire editor would throw away unsaved local edits. This is the same section-level discipline used for AI results; see [Applying AI updates safely](/guides/editor-integration#applying-ai-updates-safely-without-losing-user-edits). +3. **Advance your cursor** to the event's `id` so the next poll doesn't replay it. + + + **Don't write back a change you just received.** Applying an incoming `document_updated` into your editor will fire your editor's own change handler. If that handler autosaves, you'll re-commit the change you were just told about GÇö producing an echo loop and a spurious version bump for every other session. Suppress autosave while you apply a received change (a short "applying remote" flag around the transaction), or diff against what you already hold and save only genuinely local edits. + + +## Saving human edits (autosave) + +Pure typing GÇö edits a human makes without an AI turn GÇö is persisted through the autosave endpoint: + +``` +POST /v1/sessions/{session_id}/documents/{document_id}/save +``` + +Send the current editor HTML as `html`. Critically, also send **`base_html`** GÇö the document HTML as it was *before* this round of edits (the last version you loaded or saved) GÇö so the change is recorded precisely (only what actually changed) instead of as a blunt whole-document overwrite. Omitting `base_html` still saves but loses that precision. + +```bash theme={null} +curl -X POST \ + https://api.superdocs.app/v1/sessions/my-session/documents/{document_id}/save \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"html": "

editedGǪ

", "base_html": "

originalGǪ

"}' +``` + +Practical rules: + +* **Debounce and coalesce per document.** Don't fire a save on every keystroke. Debounce (and save on blur), and collapse rapid edits into one save per document so you're not racing yourself. After a successful save, the HTML you just sent becomes the new `base_html` for the next round. +* **This endpoint is non-AI and REST-only.** It saves typing without running a chat turn, and it isn't an MCP tool GÇö it's a UI affordance. The AI-edit flow is unaffected: an AI result is reconciled with the latest saved content, so a concurrent autosave is never silently overwritten. +* A successful save is itself a change other sessions will see on their next `doc-events` poll GÇö which is why suppressing write-back on *received* changes (above) matters. + +## Resume awareness + +When a user (or a worker) comes back to a session after being away, you want to know whether anything changed while they were gone. The roster supports this on resume: + +``` +GET /v1/sessions/{session_id}/documents?report_changed=true +``` + +With `report_changed=true`, each document carries **`changed_since_seen`** GÇö `true` when its committed version is newer than the version *this* session last saw. Use it to show a one-time "changed since you were last here" notice on resume or when switching back to a session. Set it **only** on resume / session-switch, not on the ongoing 1GÇô2s poll GÇö the live `doc-events` feed already covers changes that happen while you're watching. + +## Conflict resolution + +The hard case is when the **same section** is edited by both sides at once GÇö the user typed in a paragraph while an AI turn (or another session) rewrote it. Don't silently pick a winner by reloading. + +* **Merge, keeping the user's in-progress edits.** Apply the incoming change to every section *except* the one the user is actively editing, and for the conflicted section keep the user's version and surface the other version as an accept-able suggestion. The user's keystrokes should win by default; they can accept the alternative explicitly. This is the same conflict posture as [applying AI results](/guides/editor-integration#applying-ai-updates-safely-without-losing-user-edits). +* **When the same field gets two different values, keep the user's** GÇö never concatenate the two into one blob. +* **Never text-merge a non-text node.** Tables, diagrams (Mermaid), drawings, and equations (KaTeX/LaTeX) are structured nodes GÇö splicing one side's text into the other corrupts them. Compare and resolve those **whole**: take one version or the other for the node, don't word-diff their markup. (This is the same rule as rendering and diffing them in an editor; see [Rendering visual content](/guides/editor-integration#rendering-visual-content-diagrams-and-equations).) + +### Explicit per-section conflict resolution + +If you run your own editor and want to resolve a single conflicted section deliberately GÇö rather than relying on the automatic merge above GÇö call: + +``` +POST /v1/sessions/{session_id}/chunks/{chunk_id}/re-edit +``` + +Use it when a user edited a section while the AI was also changing it. Pick one of two outcomes with `mode`: + +* **`"redo"`** (default) GÇö re-apply the AI's intended change on top of the user's *current* text. The user's edits stay; the AI's change is layered back over them. +* **`"merge"`** GÇö blend the user's version and the AI's version into one combined section. + +| Field | Required | Meaning | +| ---------------------- | -------- | -------------------------------------------- | +| `user_current_html` | yes | The user's current version of the section. | +| `ai_proposed_new_html` | GÇö | The AI's proposed version of the section. | +| `ai_original_old_html` | GÇö | What the AI started from (its before-state). | +| `ai_explanation` | GÇö | The AI's stated intent for the change. | +| `mode` | GÇö | `"redo"` (default) or `"merge"`. | +| `model_tier` | GÇö | Optional model-tier override. | + +The response returns the rewritten section HTML only GÇö apply it to that one `data-chunk-id` block the same way you apply any other section update. + +```bash theme={null} +curl -X POST \ + https://api.superdocs.app/v1/sessions/my-session/chunks/{chunk_id}/re-edit \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "user_current_html": "

the user's current textGǪ

", + "ai_proposed_new_html": "

the AI's versionGǪ

", + "ai_original_old_html": "

what the AI started fromGǪ

", + "ai_explanation": "Tighten the wording and fix the date.", + "mode": "redo" + }' +``` + + + **This is optional, and it's a billable AI edit.** It runs one AI edit and counts as **one billable operation**. The automatic merge described above already resolves most conflicts on its own GÇö reach for `re-edit` only when you want explicit, deliberate control over one section. It is a REST endpoint, **not** an MCP/agent tool. + + +## Restoring a removed document + +If a `document_removed` event arrives for a document the user is mid-edit in, archiving it out from under them would lose their unsaved work. Offer a keep-editing choice instead of dropping the tab silently. Choosing "keep editing" restores the document and persists the user's current content so the document converges for everyone: + +``` +POST /v1/sessions/{session_id}/documents/{document_id}/unarchive +``` + +Send the editor's current `html` (same body shape as `/save`). It un-archives the document, re-links it to this session, and **saves the latest content** GÇö idempotent if another session already restored it. This session-scoped, keep-editing form is non-billable and isn't an MCP tool GÇö it's a UI affordance for the mid-edit case, because it persists the in-progress `html` as part of the restore. + +For a plain restore that doesn't carry editor content GÇö restoring an archived document by its durable id from anywhere GÇö use `POST /v1/documents/{document_id}/unarchive`, which is also the [`unarchive_document` MCP tool](/mcp/available-tools) (non-billable). An agent can restore a document with it; the session-scoped form above is the one to use only when you need to preserve a user's unsaved edits during the restore. + +## Putting it together + +A robust concurrent-editing loop, per open document: + +1. **Poll** `GET .../doc-events?after_id={cursor}` every 1GÇô2s (add `include_own=true` if you write on a separate connection). Advance the cursor. +2. On `document_updated`, **re-fetch** that document and **apply changed sections** GÇö with autosave suppressed during the apply. +3. **Debounce + coalesce** human edits and **autosave** them with `html` + `base_html`. +4. On a same-section clash, **merge and keep the user's edits**; resolve non-text nodes whole. +5. On **resume**, read the roster with `report_changed=true` and surface a one-time "changed while away" notice. + +## Stuck? + +If your collaboration shape isn't covered here GÇö a different conflict policy, presence indicators, true real-time sync GÇö email [hello@superdocs.app](mailto:hello@superdocs.app) or book a 15-minute integration call at [cal.com/superdocs](https://cal.com/superdocs). + +## Related guides + +* [Editor Integration](/guides/editor-integration) GÇö preserving `data-chunk-id` and applying updates per section. +* [Multi-Document Sessions](/guides/multi-document) GÇö several documents open in one session, and applying a revert per document. +* [SSE Streaming](/guides/streaming) GÇö your own turn's live events (`documents_changed`, `proposed_change_batch`). +* [Server Integration](/guides/server-integration) GÇö when one of the concurrent writers is a headless backend job. + + +# Editor Integration +Source: https://docs.superdocs.app/guides/editor-integration + +Make any rich-text editor round-trip SuperDocs document IDs, so surgical edits work reliably. Drop-in snippets for ProseMirror, TipTap, Slate, Lexical, Quill, and CKEditor 5. + +# Editor Integration + +**Quick path:** new to the API? Go from an API key to your first successful call in the [Quickstart](/introduction/quickstart) (\~5 minutes), then come back here to wire up your editor. + +SuperDocs identifies every block-level element in your document with a `data-chunk-id` attribute. Your editor must preserve those attributes across the round-trip GÇö HTML in, editor model, HTML back out GÇö for surgical edits to work. + + + This guide covers **attribute preservation in a rich-text editor**. For rendering inline diff overlays inside the editor, see [Human-in-the-Loop GåÆ Rendering diffs inline in your editor](/guides/human-in-the-loop#rendering-diffs-inline-in-your-editor). For streaming events, see [SSE Streaming](/guides/streaming). If your product doesn't have an editor GÇö your AI agent is the consumer of SuperDocs, or you're running server-side / batch GÇö see [Agent Tool Integration](/guides/agent-tool-integration) or [Server Integration](/guides/server-integration) instead. + + +## Why this matters + +Every SuperDocs response returns HTML like this: + +```html theme={null} +

Section 1

+

GǪ

+``` + +The `data-chunk-id` values are how the AI references specific sections when it proposes edits. When your app sends the document back on the next turn, those same IDs must still be on the same blocks. If they aren't, the AI's "edit Section 2" request lands on the wrong block GÇö or nothing at all. + +**The failure mode is silent.** Most rich-text editors strip unknown HTML attributes by default when parsing input into their internal model, and/or don't emit them when serialising back to HTML. Edits work intermittently on blocks whose tags happen to survive, and fail mysteriously on the rest. No error is thrown. A five-minute check now saves hours of debugging later. + +## The pattern, in plain English + +Every editor integration follows the same three steps: + +1. **Parse** incoming HTML from SuperDocs into your editor's document model, preserving `data-chunk-id` on every block-level node. +2. **Serialise** the editor model back to HTML, preserving the same `data-chunk-id` attributes. +3. **Render** incoming updates. `document_sync.content` (which arrives before the AI starts) can safely replace the whole document. For the AI's *results*, prefer **section-level application** over whole-document replacement GÇö see [Applying AI updates safely](#applying-ai-updates-safely-without-losing-user-edits) below for why and how. + +The snippets below implement these three steps for the six most common editors. Pick the one matching your stack, paste it in, and verify the round-trip with the test at the bottom of this page. + +## Working snippets + + + + Install the vanilla ProseMirror packages: + + ```bash theme={null} theme={null} + npm install prosemirror-state prosemirror-view prosemirror-model \ + prosemirror-schema-basic prosemirror-schema-list prosemirror-history \ + prosemirror-keymap prosemirror-commands + ``` + + Your schema needs to do two things: + + 1. Add `data-chunk-id` as a preserved attribute on every standard block node (paragraph, heading, list, blockquote, code block, horizontal rule). + 2. Register a **wrapper node** for `
GǪ
` elements. SuperDocs uses these wrappers when a single chunk spans multiple block elements (e.g. a heading plus the paragraphs that follow it). If your schema has no node that matches `div[data-chunk-id]`, the parser will descend into the children, the wrapper's `data-chunk-id` will be silently dropped, and any inline-diff or chunk-targeted edit referencing that ID will fail to render. The basic ProseMirror schema does not include a `
` node GÇö you must add one. + + ```typescript theme={null} theme={null} + // schema.ts + import { Schema, NodeSpec } from "prosemirror-model"; + import { schema as basicSchema } from "prosemirror-schema-basic"; + import { addListNodes } from "prosemirror-schema-list"; + + // TS note: prosemirror-model's ParseRule.getAttrs signature has shifted + // across versions. If your compiler complains about the parameter type, + // leave the signature alone and add `as HTMLElement` at the usage site GÇö + // do not refactor the helper. + function withChunkId(spec: NodeSpec): NodeSpec { + const attrs = { ...(spec.attrs ?? {}), "data-chunk-id": { default: null } }; + + const parseDOM = (spec.parseDOM ?? []).map((rule) => ({ + ...rule, + getAttrs: (node: string | HTMLElement) => { + const base = + typeof rule.getAttrs === "function" + ? rule.getAttrs(node as HTMLElement) ?? {} + : rule.attrs ?? {}; + if (typeof node === "string") return base; + return { ...base, "data-chunk-id": node.getAttribute("data-chunk-id") }; + }, + })); + + const originalToDOM = spec.toDOM; + const toDOM: NodeSpec["toDOM"] = originalToDOM + ? (node) => { + const out = originalToDOM(node); + if (!Array.isArray(out)) return out; + const [tag, maybeAttrs, ...rest] = out as [string, unknown, ...unknown[]]; + const chunkId = node.attrs["data-chunk-id"]; + if (!chunkId) return out; + const isAttrs = + maybeAttrs && + typeof maybeAttrs === "object" && + !Array.isArray(maybeAttrs) && + !(maybeAttrs as { nodeType?: unknown }).nodeType; + return isAttrs + ? [tag, { ...(maybeAttrs as Record), "data-chunk-id": chunkId }, ...rest] + : [tag, { "data-chunk-id": chunkId }, maybeAttrs, ...rest]; + } + : undefined; + + return { ...spec, attrs, parseDOM, toDOM }; + } + + // Wrapper node for multi-element chunks:
GǪ
. + // Holds one or more block children; preserves the chunk-id on round-trip. + const chunkWrapperSpec: NodeSpec = { + group: "block", + content: "block+", + attrs: { "data-chunk-id": { default: null } }, + parseDOM: [{ + tag: "div[data-chunk-id]", + getAttrs: (node) => + typeof node === "string" + ? {} + : { "data-chunk-id": node.getAttribute("data-chunk-id") }, + }], + toDOM: (node) => [ + "div", + node.attrs["data-chunk-id"] + ? { "data-chunk-id": node.attrs["data-chunk-id"] } + : {}, + 0, + ], + }; + + let nodes = basicSchema.spec.nodes; + for (const name of ["paragraph", "blockquote", "heading", "horizontal_rule", "code_block"]) { + const spec = nodes.get(name); + if (spec) nodes = nodes.update(name, withChunkId(spec)); + } + nodes = addListNodes(nodes, "paragraph block*", "block"); + for (const name of ["ordered_list", "bullet_list", "list_item"]) { + const spec = nodes.get(name); + if (spec) nodes = nodes.update(name, withChunkId(spec)); + } + nodes = nodes.addToEnd("chunk_wrapper", chunkWrapperSpec); + + export const schema = new Schema({ nodes, marks: basicSchema.spec.marks }); + ``` + + Use this `schema` when creating your `EditorState`. Use `DOMParser.fromSchema(schema).parse(htmlElement)` to load SuperDocs HTML and `DOMSerializer.fromSchema(schema).serializeFragment(doc.content)` to serialise it back out. + + **Where this slots in:** typically inside a React / Vue / Svelte component that mounts ProseMirror via `new EditorView(domNode, { state })`. Expose two methods on a ref GÇö `getHtml()` and `setHtml(html)` GÇö so the chat panel can read the current document before each send and write the updated HTML after each `final` event. + + + + Install TipTap: + + ```bash theme={null} theme={null} + npm install @tiptap/react @tiptap/starter-kit + ``` + + You need two pieces: + + 1. A **global-attribute extension** that preserves `data-chunk-id` on every standard block node. + 2. A **wrapper Node** for `
GǪ
` elements. SuperDocs uses these wrappers when a single chunk spans multiple block elements (e.g. a heading plus the paragraphs that follow it). TipTap's StarterKit has no node that matches a `
`, so without this extra Node the wrapper's `data-chunk-id` is silently dropped during parsing GÇö and any inline-diff or chunk-targeted edit referencing that ID will fail to render. + + ```typescript theme={null} theme={null} + // chunk-id-preserver.ts + import { Extension, Node } from "@tiptap/core"; + + export const ChunkIdPreserver = Extension.create({ + name: "chunkIdPreserver", + addGlobalAttributes() { + return [{ + types: [ + "paragraph", + "heading", + "blockquote", + "bulletList", + "orderedList", + "listItem", + "codeBlock", + "horizontalRule", + ], + attributes: { + "data-chunk-id": { + default: null, + parseHTML: (el) => el.getAttribute("data-chunk-id"), + renderHTML: (attrs) => + attrs["data-chunk-id"] + ? { "data-chunk-id": attrs["data-chunk-id"] } + : {}, + }, + }, + }]; + }, + }); + + // Wrapper Node for multi-element chunks:
GǪ
. + export const ChunkWrapper = Node.create({ + name: "chunkWrapper", + group: "block", + content: "block+", + parseHTML() { + return [{ tag: "div[data-chunk-id]" }]; + }, + renderHTML({ HTMLAttributes }) { + return ["div", HTMLAttributes, 0]; + }, + addAttributes() { + return { + "data-chunk-id": { + default: null, + parseHTML: (el) => el.getAttribute("data-chunk-id"), + renderHTML: (attrs) => + attrs["data-chunk-id"] + ? { "data-chunk-id": attrs["data-chunk-id"] } + : {}, + }, + }; + }, + }); + ``` + + Register both on your editor: + + ```typescript theme={null} theme={null} + const editor = useEditor({ + extensions: [StarterKit, ChunkIdPreserver, ChunkWrapper], + content: superDocsHtml, + }); + ``` + + Then `editor.getHTML()` returns the round-tripped HTML, and `editor.commands.setContent(html, false)` loads a new SuperDocs payload without breaking undo history. + + **Where this slots in:** anywhere you already use `useEditor`. If you use additional node extensions (tables, images, custom blocks), add their type names to the `types` array. + + + + Install Slate: + + ```bash theme={null} theme={null} + npm install slate slate-react slate-history + ``` + + Declare your element types with an optional `data-chunk-id` field and provide HTML serialisation helpers: + + ```typescript theme={null} theme={null} + // slate-chunk-id.ts + import { Element as SlateElement, Descendant } from "slate"; + + export type ChunkedElement = SlateElement & { + type: string; + "data-chunk-id"?: string | null; + children: { text: string }[]; + }; + + // Render incoming SuperDocs HTML into Slate nodes. + export function fromHtml(html: string): ChunkedElement[] { + const doc = new DOMParser().parseFromString(html, "text/html"); + return Array.from(doc.body.children).map((el) => ({ + type: el.tagName.toLowerCase(), + "data-chunk-id": el.getAttribute("data-chunk-id"), + children: [{ text: el.textContent ?? "" }], + })) as ChunkedElement[]; + } + + // Serialise Slate nodes back to HTML, preserving data-chunk-id. + export function toHtml(nodes: ChunkedElement[]): string { + return nodes + .map((n) => { + const id = n["data-chunk-id"] ? ` data-chunk-id="${n["data-chunk-id"]}"` : ""; + const tag = n.type ?? "p"; + const inner = n.children.map((c) => c.text ?? "").join(""); + return `<${tag}${id}>${inner}`; + }) + .join(""); + } + ``` + + In your `renderElement`, pass `data-chunk-id` through on the outer DOM element: + + ```tsx theme={null} theme={null} + function renderElement({ attributes, children, element }: RenderElementProps) { + const chunkId = (element as ChunkedElement)["data-chunk-id"]; + const Tag = (element as ChunkedElement).type as keyof JSX.IntrinsicElements; + return ( + + {children} + + ); + } + ``` + + **Where this slots in:** call `fromHtml()` whenever you receive a `document_sync` or `final` event, pass the result to `Editor.withoutNormalizing` or `Transforms.insertNodes` as appropriate, and call `toHtml(editor.children)` before every chat send. + + + + Install Lexical: + + ```bash theme={null} theme={null} + npm install lexical @lexical/react @lexical/html + ``` + + Lexical doesn't have a first-class "preserve unknown attributes" feature GÇö the cleanest pattern is a `WeakMap` that stores chunk IDs against nodes, with `exportDOM` overrides on the block node types you care about. + + ```typescript theme={null} theme={null} + // chunk-id-store.ts + import { LexicalNode, ElementNode } from "lexical"; + import { $generateNodesFromDOM, $generateHtmlFromNodes } from "@lexical/html"; + + const chunkIds = new WeakMap(); + + export const setChunkId = (node: LexicalNode, id: string | null) => { + if (id) chunkIds.set(node, id); + else chunkIds.delete(node); + }; + export const getChunkId = (node: LexicalNode) => chunkIds.get(node) ?? null; + + // Import: walk DOM + Lexical nodes in parallel and record chunk IDs. + export function importHtmlWithChunkIds(editor: any, html: string) { + editor.update(() => { + const doc = new DOMParser().parseFromString(html, "text/html"); + const nodes = $generateNodesFromDOM(editor, doc); + const blocks = doc.body.querySelectorAll("[data-chunk-id]"); + blocks.forEach((el, i) => { + const id = el.getAttribute("data-chunk-id"); + if (nodes[i] && id) setChunkId(nodes[i], id); + }); + // Replace editor contents with nodes. + }); + } + + // Export: generate HTML, then inject data-chunk-id back onto block elements. + export function exportHtmlWithChunkIds(editor: any, root: ElementNode): string { + let html = ""; + editor.getEditorState().read(() => { + html = $generateHtmlFromNodes(editor, null); + }); + const doc = new DOMParser().parseFromString(`${html}`, "text/html"); + const blocks = doc.body.children; + const children = root.getChildren(); + for (let i = 0; i < blocks.length && i < children.length; i++) { + const id = getChunkId(children[i]); + if (id) blocks[i].setAttribute("data-chunk-id", id); + } + return doc.body.innerHTML; + } + ``` + + **Where this slots in:** call `importHtmlWithChunkIds` on `document_sync` and `final`, and `exportHtmlWithChunkIds` before every chat send. Lexical's model makes this the most custom wiring of any editor on this page GÇö verify the round-trip carefully with the test at the bottom. + + + + Install Quill: + + ```bash theme={null} theme={null} + npm install quill + ``` + + Register a Parchment block-level attributor for `data-chunk-id`: + + ```typescript theme={null} theme={null} + // quill-chunk-id.ts + import Quill from "quill"; + const Parchment = Quill.import("parchment") as typeof import("parchment"); + + const ChunkIdAttributor = new Parchment.Attributor.Attribute( + "data-chunk-id", + "data-chunk-id", + { scope: Parchment.Scope.BLOCK } + ); + Quill.register(ChunkIdAttributor, true); + ``` + + Load SuperDocs HTML via the clipboard API (Parchment preserves block attributes by default once the attributor is registered): + + ```typescript theme={null} theme={null} + quill.clipboard.dangerouslyPasteHTML(superDocsHtml); + ``` + + Read the round-tripped HTML with `quill.root.innerHTML` GÇö `data-chunk-id` stays on every block. + + **Where this slots in:** register the attributor once at startup, before any editor is instantiated. Use `clipboard.dangerouslyPasteHTML` for every `document_sync` and `final` event. + + + + Install the CKEditor 5 base packages: + + ```bash theme={null} theme={null} + npm install @ckeditor/ckeditor5-core @ckeditor/ckeditor5-engine + ``` + + CKEditor 5 uses a schema + conversion model. Register `data-chunk-id` as an allowed attribute on every block element and set up conversion both ways: + + ```typescript theme={null} theme={null} + // chunk-id-plugin.ts + import { Plugin } from "@ckeditor/ckeditor5-core"; + + export default class ChunkIdPreserver extends Plugin { + static get pluginName() { return "ChunkIdPreserver"; } + + init() { + const editor = this.editor; + const schema = editor.model.schema; + + // Allow the attribute on every block-level element. + schema.extend("$block", { allowAttributes: "chunkId" }); + + // Downcast: model GåÆ view + editor.conversion.for("downcast").attributeToAttribute({ + model: "chunkId", + view: "data-chunk-id", + }); + + // Upcast: view GåÆ model + editor.conversion.for("upcast").attributeToAttribute({ + view: { name: /.+/, key: "data-chunk-id" }, + model: "chunkId", + }); + } + } + ``` + + Register it on your editor: + + ```typescript theme={null} theme={null} + ClassicEditor.create(element, { + plugins: [..., ChunkIdPreserver], + }); + ``` + + Then `editor.getData()` returns HTML with `data-chunk-id` preserved, and `editor.setData(html)` loads SuperDocs HTML without stripping it. + + **Where this slots in:** add `ChunkIdPreserver` to the `plugins` array of whichever CKEditor 5 build you're using. If you have custom block elements (tables, images, custom widgets), ensure `schema.extend` is called for their names too. + + + +## Applying AI updates safely (without losing user edits) + +The naive pattern GÇö replace the whole editor document with `final.updated_html` on every turn GÇö works for a demo and fails in production in two specific ways. We hit both building the SuperDocs web app; they're worth designing out of your integration from day one. + +**Failure 1 GÇö wiping concurrent user edits.** AI turns take seconds to minutes. If the user keeps typing while the AI works and you then `setContent` the AI's full document, everything they typed is silently gone. The fix is to apply changes **per section**: every result identifies exactly which chunks changed (`document_changes`, `chunk_diffs` in compact mode, or the per-change `old_html`/`new_html` in review mode), so you can replace only those `data-chunk-id` nodes and leave the rest of the document GÇö including the user's in-progress typing GÇö untouched. If the user edited the *same* section the AI changed, don't silently overwrite either version: keep the user's version and offer the AI's as a suggestion (or re-send the user's version on the next turn). The user's keystrokes should always win by default. + +**Protecting stored formatting on untouched sections.** Editor serialisers rarely reproduce a document's HTML byte-for-byte, and a purely cosmetic serialisation difference can look like a user edit. If your integration tracks which blocks the user actually modified, send that list as `touched_chunk_ids` alongside `document_html` on chat and save calls: a section not in the list whose text is unchanged keeps its stored formatting exactly, even if your serialiser emitted it differently. Sections in the list, and any section whose text changed, always take your submitted content. Omit the field for the default behaviour (any differing section is treated as an edit). + +**Failure 2 GÇö double-applying in review mode.** In `ask_every_time` mode, if your UI applies each change to the editor when the user accepts it, remember the job still finishes with a `final` event carrying the complete updated document. Applying accepted changes incrementally **and** then loading the final document re-applies everything GÇö newly created sections get inserted twice. Pick one strategy and stick to it: **either** apply per-change on accept and ignore the final document, **or** keep the editor untouched during review and load only the final. Never both. + +**Revert and redo flow through the same path.** When you rewind a session with `POST /v1/sessions/{id}/revert`, the response carries a per-document `revert_changes` map. Apply it section-by-section exactly like an AI result rather than reloading the whole document, so any in-progress typing survives and a clashed section can keep the user's version and surface the other as an accept-able suggestion. Redo (`POST /v1/sessions/{id}/redo`) returns the same shape. Dry-run revert (`"dry_run": true`) returns the changes without committing, so you can preview it first. Full detail in [Reverting without a whole-document swap](/guides/multi-document#reverting-without-a-whole-document-swap). + +Whole-document replacement remains fine for read-only/display integrations, or if you lock the editor while an AI turn is running. + + + If two of your surfaces (e.g. an editor tab and a server job, or two browser sessions) can edit the **same** document, the same section-level discipline applies across connections GÇö poll for other sessions' edits and re-apply them per section rather than reloading the whole document. See [Concurrent & Cross-Session Editing](/guides/concurrent-editing). + + +## Rendering visual content (diagrams and equations) + +SuperDocs documents can contain diagrams (Mermaid), equations (LaTeX/KaTeX), drawings, and images. On the wire these arrive as HTML nodes that carry their **source in `data-` attributes** (e.g. the diagram's text spec, the equation's LaTeX) plus the node markup itself. + +* **Preserve the `data-` attributes on round-trip** GÇö exactly the same rule (and the same silent failure mode) as `data-chunk-id`. Most editor schemas and HTML sanitizers strip unknown attributes and `` by default; allow them through, or diagrams will degrade to empty blocks after one edit cycle. +* **Render client-side after mount.** Use the standard libraries (Mermaid for diagrams, KaTeX for equations) to render the source into SVG/HTML once the node is in the DOM. Poll-free pattern: render on insert + on change of the source attribute. +* **Render into a container your UI framework doesn't manage.** If your framework (React, Vue, etc.) believes it owns the rendered markup, its next reconciliation pass can silently wipe the out-of-band SVG you just produced GÇö this exact bug shipped in our own web app before we caught it. Mount the rendered output inside an element the framework treats as opaque (a ref'd container you never re-render declaratively), not via an HTML-binding prop. +* **You never need to rasterize for export** GÇö `POST /v1/documents/export` pre-renders diagrams and equations server-side in every format. + +## Out-of-flow parts (headers, footers, footnotes, comments) + +A document with page headers, footers, footnote/endnote bodies, or reviewer comments carries them as ordinary blocks labeled with a `data-part-type` attribute (alongside their `data-chunk-id`). They hold out-of-flow content, so they aren't body text: + +* **Preserve them like any other block.** Same parse/serialise rule (and the same silent failure mode) as `data-chunk-id`: keep the `data-part-type` attribute and the block intact on the round-trip. +* **Render them appropriately, or not at all.** A footer block is not a trailing paragraph. If your UI has no header/footer affordance, it's fine to hide these blocks; just keep them in the HTML you send back. +* **Leaving one out never deletes it.** The server treats an out-of-flow part's absence from `document_html` as normal (an editor view without the footer is the usual state), so a client that strips them can't destroy them. To actually delete one, pass its chunk id in the `deleted_part_chunk_ids` request field alongside `document_html`. +* **Users edit them by chat** like any other content ("fix the typo in footnote 3"). In review mode the proposal arrives as a normal entry in `proposed_change_batch`, targeting the part's `chunk_id`. + +## Multi-document sessions in your editor + +Sessions can hold several open documents (see the [Multi-Document guide](/guides/multi-document)). If your product surfaces this, the editor-side pattern is: + +* Render **tabs** from `GET /v1/sessions/{id}/documents` (stable insertion order; use the `title` field each roster entry returns for the tab label). The roster is token-light by default GÇö it returns metadata only and omits each document's HTML body (`null`); pass `include_html=true` when you actually need the content. +* Switch tabs with the focus endpoint; pass `document_id` on chat turns initiated from a specific tab. +* Listen for `documents_changed` to **badge background tabs** the AI touched, and to **add a tab** when an entry carries `created: true` (this fires in either approval mode for created documents). +* If a document payload includes `page_setup`, you can render true page geometry (size, orientation, margins) instead of a generic canvas. + +## Other editors and custom implementations + +The principle is the same regardless of editor: preserve unknown HTML attributes on block-level elements across parse and serialise. + +Three things to verify in your editor's documentation: + +1. Does the **parser** strip unknown attributes by default? Most do. Look for "custom attributes" or "attribute preservation" in the docs. +2. Does the **serialiser** emit them when converting back to HTML? If the parser accepted them, the serialiser usually does GÇö but not always. +3. Does the **internal model** store them on every block type you plan to use? Often there's a schema or node definition that has to be extended per node type. + +### Verification test + +Paste any SuperDocs response into your editor, read the HTML back out, and diff against the input. Every `data-chunk-id` must survive. + +```javascript theme={null} theme={null} +const incoming = `

Title

Body

`; + +editor.setHtml(incoming); +const roundTripped = editor.getHtml(); + +const inIds = [...incoming.matchAll(/data-chunk-id="([^"]+)"/g)].map(m => m[1]); +const outIds = [...roundTripped.matchAll(/data-chunk-id="([^"]+)"/g)].map(m => m[1]); + +console.assert( + inIds.every(id => outIds.includes(id)), + "Chunk IDs did not survive the round-trip", + { inIds, outIds } +); +``` + +Run this on every block type your product uses GÇö headings, paragraphs, lists, list items, blockquotes, code blocks, horizontal rules, tables. A gap in any single type will surface as occasional silent edit failures in production. + +## Stuck? + +If your editor isn't covered here, or chunk-id round-trip is failing despite following the pattern, email [hello@superdocs.app](mailto:hello@superdocs.app) or book a 15-minute integration call at [cal.com/superdocs](https://cal.com/superdocs). We'll add your editor's pattern to this guide. + + +# Human-in-the-Loop +Source: https://docs.superdocs.app/guides/human-in-the-loop + +Review and approve AI-proposed document changes before they're applied, using the HITL approval workflow. + +# Human-in-the-Loop + +**Quick path:** new to the API? Go from an API key to your first successful call in the [Quickstart](/introduction/quickstart) (\~5 minutes), then come back here for the approval workflow. + +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. + + + **Maximum precision on high-stakes documents** (contracts, regulatory filings, compliance language) GÇö set `model_tier: "max"` in your request body. The default `core` tier is fast and accurate for everyday edits, but `max` gives you the most capable model for nuanced edits where one wrong word matters. See [Model Selection](/guides/model-selection) for the full matrix. + + + + This guide covers **UI-driven approval** (a human reviewing in your product's interface). If your AI agent is the one deciding whether to approve proposed changes (no human in the loop), see [Agent Tool Integration GåÆ Approval modes](/guides/agent-tool-integration#approval-modes-choose-based-on-who-decides). For batch / server-side workflows that auto-approve every change, see [Server Integration](/guides/server-integration) and pass `approval_mode: "approve_all"` instead. + + + + **Revert vs. pending approval.** While a session is `awaiting_approval` (one or more proposed changes pending review), calling [revert](/concepts/sessions#revert-a-session-to-a-previous-message) returns `409`. Resolve the pending approvals first GÇö accept or deny each change, or cancel the in-flight job via `POST /v1/jobs/{job_id}/cancel` GÇö then retry revert. + + +## Recommended UX pattern + +**Default your UI to auto-apply; expose Review Mode as an opt-in toggle.** + +The simplest, least-friction integration is to send `approval_mode: "approve_all"` by default and put a small in-UI toggle GÇö one control, clearly labelled GÇö that the user can flip on when they want to review each change before it lands. When the toggle is on, your code switches to `approval_mode: "ask_every_time"` and renders the proposed-change UI (chat-side card, inline editor overlay, or native track-changes GÇö see below). + +This is the pattern the SuperDocs web app at [use.superdocs.app](https://use.superdocs.app) uses, and it's what most users expect: + +* **Auto-apply on by default** GÇö for 90% of edits (rewording, formatting, small additions), users don't want to approve each change. They want the AI to act, then Cmd-Z if they disagree. +* **Review Mode as an explicit opt-in** GÇö when the user is working on something high-stakes (a contract clause, a regulatory filing, a legal letter) they flip the toggle ON and approve each change explicitly. Persist the toggle state in `localStorage` so it survives page reload. +* **No hidden state** GÇö both modes should be plainly visible in the UI. Don't bury Review Mode in a settings modal. + +A common concrete shape: a two-segment slider or segmented control above the chat input, with "Auto Approve" on one end and "Review Mode" on the other. The active segment is filled, the inactive segment is muted. One glance tells the user which mode they're in. + +Going straight to `ask_every_time` without a toggle is valid for high-stakes products (contracts, medical records, court filings) where every change must be reviewed, but expect higher friction in casual editing flows. If in doubt, start with auto-apply by default and a toggle GÇö you can always reverse it later. + +## End-to-end workflow + +### 1. Send a request with approval mode + +```bash theme={null} +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 + +```bash theme={null} +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`: + + + **`awaiting_approval` has two flavours GÇö continue vs. approve.** The same status is also used for the [large-edit continue prompt](/guides/async-jobs#continuing-a-large-edit), where a big edit applies as much as it can, keeps that work, and pauses to ask whether to keep going. Tell them apart with `metadata.awaiting_kind`: + + * `"continue_prompt"` GåÆ a large edit paused. There are **no** `pending_changes` (the SSE side sent a [`continue_prompt`](/guides/streaming#continue_prompt) event, not `proposed_change_batch`). Resume with `POST /v1/chat/{session_id}/continue` (the `continue_chat` tool over MCP), passing `{ "job_id": "...", "continue": true }` to keep going or `false` to stop and keep what's done. + * otherwise GåÆ a HITL change review. Read `metadata.pending_changes` and respond with `/approve`. + + Calling `/approve` on a `continue_prompt` pause is rejected with `409` (and vice-versa) GÇö always branch on `awaiting_kind` first. + + +```json theme={null} +{ + "status": "awaiting_approval", + "metadata": { + "pending_changes": [ + { + "change_id": "ch_1", + "operation": "edit", + "chunk_id": "550e8400-e29b-41d4-a716-446655440000", + "old_html": "

Original section 3 content...

", + "new_html": "

Updated content with GDPR compliance...

", + "ai_explanation": "Added GDPR data processing requirements" + } + ] + } +} +``` + +### 3. Approve or deny changes + + + **The `approved` field is required at the top level of every approve request GÇö including batch shapes.** A common integration trap is to send `{ "job_id": "...", "changes": [...] }` for a batch decision, omitting top-level `approved`. The endpoint rejects this with a generic `422` because the top-level `approved` is required by the request schema. The top-level value acts as the default for any change inside `changes` that does not specify its own `approved`. If every entry inside `changes` carries its own `approved`, the top-level value is unused but still required GÇö set it to `true` or `false`, it doesn't matter which. + + **Correct shapes (one of these three) GÇö all carry top-level `approved`:** + + * **Single change:** `{ "job_id": "...", "change_id": "...", "approved": true }` + * **Batch GÇö same decision for all:** `{ "job_id": "...", "approved": true, "changes": [{"change_id": "ch_1"}, {"change_id": "ch_2"}] }` + * **Batch GÇö per-change decisions:** `{ "job_id": "...", "approved": true, "changes": [{"change_id": "ch_1", "approved": true}, {"change_id": "ch_2", "approved": false}] }` + + **Incorrect GÇö missing top-level `approved`:** `{ "job_id": "...", "changes": [...] }` GåÆ `422 Unprocessable Entity`. + + +**Approve a single change:** + +```bash theme={null} +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:** + +```bash theme={null} +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:** + +```bash theme={null} +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` GÇö 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 | GÇö | 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 | GÇö | Remove a section from the document | + +Proposed edits to page headers, footers, footnote/endnote bodies, and comments use these same shapes: each arrives as an ordinary entry (its `chunk_id` targets the part's block) in the same batch as any body edits from the turn, so one turn can propose a body change and a header change and the user approves each independently. There is no separate approval track for them. + +### 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 GÇö 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 GÇö it tells the user why the AI proposed the change. + +### Rendering diffs inline in your editor + +Three patterns for where the diff appears, in order of visual weight: + +#### Pattern 1 GÇö Side-by-side card in your chat panel + +Render a card per pending change with `old_html` and `new_html` shown as two adjacent panels (red background for old, green background for new) and Approve / Deny buttons underneath. The card lives in your chat sidebar; the editor document is unaffected until the user approves. This is the simplest pattern and works with any editor. See the [JavaScript example](/examples/javascript) for a working HITL flow that produces this UI. + +#### Pattern 2 GÇö Inline overlay in the editor (Cursor-style) + +The proposed edit appears **directly inside the document** GÇö the affected block gets a coloured outline, the word-level diff renders inside it (red strikethrough for removed, green highlight for added), and Approve / Deny buttons float in the top-right corner of the block. This is the most accurate visual representation of what the AI wants to change and is the pattern most developer-audience apps converge on. + +For ProseMirror-based editors (ProseMirror, TipTap, BlockNote, Remirror, Atlaskit), use the editor's native decoration system. The plugin below takes individual change objects (the entries of a `proposed_change_batch` event's `changes` array), builds a `DecorationSet` keyed by `data-chunk-id`, and dispatches `window`-level custom events when the user clicks Approve or Deny. + + + **Schema prerequisite GÇö read first.** The plugin below locates the target block by walking the editor doc and matching `node.attrs["data-chunk-id"]`. If your schema does not preserve `data-chunk-id` on every block node **and** does not register a wrapper Node for `
GǪ
` elements (used when a chunk spans multiple blocks), the plugin will compile, run, and silently render nothing GÇö `findChunkRange` returns `null` and the decoration is skipped. This is the single most common reason an inline overlay implementation appears broken: the diff event arrives, `addProposedChange` fires, but the editor never lights up. + + Set up the schema first via [Editor Integration](/guides/editor-integration). Both the per-block attribute *and* the `
` wrapper Node are required GÇö neither alone is sufficient. + + +```typescript theme={null} theme={null} +// proposed-change-decoration.ts +import { Plugin, PluginKey, Transaction } from "prosemirror-state"; +import { Decoration, DecorationSet, EditorView } from "prosemirror-view"; +import type { Node as ProseMirrorNode } from "prosemirror-model"; + +export type ProposedChange = { + change_id: string; + operation: "edit" | "create" | "delete"; + chunk_id: string | null; + old_html: string | null; + new_html: string | null; + ai_explanation: string; + insert_after_chunk_id: string | null; +}; + +const META_ADD = "proposedChangeAdd"; +const META_REMOVE = "proposedChangeRemove"; +const META_CLEAR = "proposedChangeClear"; + +export const proposedChangeKey = new PluginKey("proposedChangeOverlay"); + +type State = { + decorations: DecorationSet; + pending: Map; +}; + +function findChunkRange(doc: ProseMirrorNode, chunkId: string) { + let hit: { from: number; to: number } | null = null; + doc.descendants((node, pos) => { + if (hit) return false; + if (node.attrs?.["data-chunk-id"] === chunkId) { + hit = { from: pos, to: pos + node.nodeSize }; + return false; + } + return true; + }); + return hit; +} + +function stripHtml(html: string) { + const withSpaces = html.replace(/<\/(p|div|li|h[1-6]|tr|td|th)>/gi, " "); + const tmp = document.createElement("div"); + tmp.innerHTML = withSpaces; + return (tmp.textContent ?? "").replace(/\s+/g, " ").trim(); +} + +function escapeHtml(s: string) { + return s.replace(/&/g, "&").replace(//g, ">"); +} + +// Word-level LCS diff GåÆ red/green HTML. +function wordDiff(oldText: string, newText: string) { + const A = oldText.split(/(\s+)/); + const B = newText.split(/(\s+)/); + const m = A.length, n = B.length; + const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)); + for (let i = 1; i <= m; i++) + for (let j = 1; j <= n; j++) + dp[i][j] = A[i - 1] === B[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]); + const parts: { t: "same" | "del" | "add"; s: string }[] = []; + let i = m, j = n; + while (i > 0 || j > 0) { + if (i > 0 && j > 0 && A[i - 1] === B[j - 1]) { parts.unshift({ t: "same", s: A[i - 1] }); i--; j--; } + else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) { parts.unshift({ t: "add", s: B[j - 1] }); j--; } + else { parts.unshift({ t: "del", s: A[i - 1] }); i--; } + } + return parts.map(p => { + const e = escapeHtml(p.s); + if (p.t === "del") return `${e}`; + if (p.t === "add") return `${e}`; + return e; + }).join(""); +} + +function controls(change: ProposedChange): HTMLDivElement { + const wrap = document.createElement("div"); + wrap.className = "inline-diff-controls"; + wrap.contentEditable = "false"; + for (const action of ["accept", "deny"] as const) { + const btn = document.createElement("button"); + btn.className = `inline-diff-btn inline-diff-btn--${action}`; + btn.textContent = action === "accept" ? "\u2713" : "\u2717"; + btn.addEventListener("mousedown", e => { e.preventDefault(); e.stopPropagation(); }); + btn.addEventListener("click", e => { + e.preventDefault(); e.stopPropagation(); + window.dispatchEvent(new CustomEvent("diff-action", { + detail: { action, change_id: change.change_id }, + })); + }); + wrap.appendChild(btn); + } + return wrap; +} + +function editWidget(pos: number, change: ProposedChange): Decoration { + return Decoration.widget(pos, () => { + const wrap = document.createElement("div"); + wrap.className = "diff-edit-wrapper"; + wrap.contentEditable = "false"; + wrap.setAttribute("data-diff-widget", "true"); + wrap.appendChild(controls(change)); + const preview = document.createElement("div"); + preview.className = "diff-preview-content"; + preview.innerHTML = wordDiff(stripHtml(change.old_html ?? ""), stripHtml(change.new_html ?? "")); + wrap.appendChild(preview); + return wrap; + }, { side: -1, key: `diff-edit-${change.change_id}` }); +} + +function ghostCreateWidget(pos: number, change: ProposedChange): Decoration { + return Decoration.widget(pos, () => { + const ghost = document.createElement("div"); + ghost.className = "diff-ghost-create"; + ghost.contentEditable = "false"; + ghost.appendChild(controls(change)); + const body = document.createElement("div"); + body.className = "diff-ghost-content"; + body.innerHTML = change.new_html ?? "

New section

"; + ghost.appendChild(body); + return ghost; + }, { side: 1, key: `diff-ghost-${change.change_id}` }); +} + +function build(doc: ProseMirrorNode, pending: Map): DecorationSet { + const out: Decoration[] = []; + for (const change of pending.values()) { + if (change.operation === "create") { + if (!change.insert_after_chunk_id) continue; + const after = findChunkRange(doc, change.insert_after_chunk_id); + if (after) out.push(ghostCreateWidget(after.to, change)); + continue; + } + if (!change.chunk_id) continue; + const range = findChunkRange(doc, change.chunk_id); + if (!range) continue; + if (change.operation === "edit") { + const hasPreview = !!(change.old_html && change.new_html); + out.push(Decoration.node(range.from, range.to, { + class: hasPreview ? "diff-chunk-edit diff-chunk-has-preview" : "diff-chunk-edit", + })); + if (hasPreview) out.push(editWidget(range.from + 1, change)); + } else if (change.operation === "delete") { + out.push(Decoration.node(range.from, range.to, { class: "diff-chunk-delete" })); + } + } + return DecorationSet.create(doc, out); +} + +export function proposedChangeDecoration(): Plugin { + return new Plugin({ + key: proposedChangeKey, + state: { + init() { return { decorations: DecorationSet.empty, pending: new Map() }; }, + apply(tr: Transaction, value: State): State { + let pending = value.pending; + const add = tr.getMeta(META_ADD) as ProposedChange | undefined; + const remove = tr.getMeta(META_REMOVE) as string | undefined; + const clear = tr.getMeta(META_CLEAR) as boolean | undefined; + if (clear) return { decorations: DecorationSet.empty, pending: new Map() }; + if (add) { pending = new Map(pending); pending.set(add.change_id, add); } + if (remove) { pending = new Map(pending); pending.delete(remove); } + if (add || remove) return { decorations: build(tr.doc, pending), pending }; + if (tr.docChanged) return { decorations: value.decorations.map(tr.mapping, tr.doc), pending }; + return value; + }, + }, + props: { decorations(state) { return proposedChangeKey.getState(state)?.decorations ?? DecorationSet.empty; } }, + }); +} + +export const addProposedChange = (view: EditorView, change: ProposedChange) => + view.dispatch(view.state.tr.setMeta(META_ADD, change)); +export const removeProposedChange = (view: EditorView, change_id: string) => + view.dispatch(view.state.tr.setMeta(META_REMOVE, change_id)); +export const clearProposedChanges = (view: EditorView) => + view.dispatch(view.state.tr.setMeta(META_CLEAR, true)); +``` + +Supporting CSS: + +```css theme={null} theme={null} +.diff-chunk-edit { + outline: 2px solid #4285f4; outline-offset: -1px; + background: rgba(66, 133, 244, 0.06); border-radius: 4px; position: relative; +} +.diff-chunk-has-preview > *:not([data-diff-widget]) { display: none !important; } +.diff-chunk-delete { + outline: 2px solid #dc3545; outline-offset: -1px; + background: rgba(220, 53, 69, 0.06); border-radius: 4px; position: relative; +} +.diff-chunk-delete > *:not([data-diff-widget]) { + text-decoration: line-through; text-decoration-color: rgba(220, 53, 69, 0.5); opacity: 0.65; +} +.inline-diff-controls { + position: absolute; top: 4px; right: 4px; display: flex; gap: 4px; + background: white; border: 1px solid #e0e0e0; border-radius: 6px; + padding: 3px 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.12); z-index: 50; user-select: none; +} +.inline-diff-btn { + width: 32px; height: 32px; border: none; border-radius: 4px; + font-size: 18px; font-weight: 700; cursor: pointer; + display: flex; align-items: center; justify-content: center; line-height: 1; padding: 0; +} +.inline-diff-btn--accept { background: #e6ffed; color: #28a745; } +.inline-diff-btn--accept:hover { background: #28a745; color: white; } +.inline-diff-btn--deny { background: #ffeef0; color: #dc3545; } +.inline-diff-btn--deny:hover { background: #dc3545; color: white; } +.diff-edit-wrapper { position: relative; padding: 44px 8px 8px 8px; } +.diff-preview-content { line-height: 1.6; white-space: pre-wrap; word-wrap: break-word; } +.diff-word-deleted { + background: rgba(220, 53, 69, 0.15); color: #b71c1c; + text-decoration: line-through; text-decoration-color: #dc3545; + padding: 1px 2px; border-radius: 2px; +} +.diff-word-added { + background: rgba(40, 167, 69, 0.15); color: #1b5e20; + text-decoration: underline; text-decoration-color: #28a745; + padding: 1px 2px; border-radius: 2px; +} +.diff-ghost-create { + position: relative; border: 2px dashed #28a745; background: rgba(40, 167, 69, 0.04); + border-radius: 6px; padding: 44px 8px 8px 8px; margin: 8px 0; +} +.diff-ghost-content { opacity: 0.75; line-height: 1.6; } +``` + +Wire the plugin into your editor and the chat panel: + +* Register `proposedChangeDecoration()` in the plugin array when you build `EditorState`. +* On every `proposed_change_batch` SSE event in the chat panel, loop over the parsed `changes` array and call `addProposedChange(view, change)` for each one. +* Subscribe to `window.addEventListener("diff-action", ...)` in the chat panel to catch Accept / Deny clicks. Post the decision to `/v1/chat/{session_id}/approve` and call `removeProposedChange(view, change_id)`. +* On every `final` SSE event, call `clearProposedChanges(view)` before applying the new HTML GÇö the editor is about to re-render anyway. + +The pattern transfers to TipTap, BlockNote, Remirror, and Atlaskit with minor API-surface tweaks GÇö they all expose the same underlying decoration system. + +#### Pattern 3 GÇö Native track-changes UI + +If your editor already supports track-changes (CKEditor 5 with the Track Changes feature, TinyMCE Premium, etc.), map each proposed change (each entry of the `proposed_change_batch` event's `changes` array) to a tracked suggestion in the editor's native model. The user then approves or rejects via the editor's built-in UI. Consult your editor's track-changes API docs for the specific mapping GÇö the SuperDocs side is identical to Pattern 1 or 2. + +### Batch changes + +When the AI proposes multiple changes at once (common for sweeping edits across many sections), every change in the turn arrives together. The shape depends on whether you're polling or streaming: + +* **Polling**: all changes appear in the `metadata.pending_changes` array on a single `/v1/jobs/{job_id}` poll. Each change carries `batch_id` (the first change's `change_id`) and `batch_total` (the count) for convenience. +* **Streaming (recommended)**: a single `proposed_change_batch` SSE event carries the full batch as a `changes` array. See [Streaming guide GåÆ `proposed_change_batch`](/guides/streaming#proposed_change_batch). + +Use the batched shape to render one "Accept all" / "Deny all" card without waiting on a fan-out of N events. + +### Grouping a batch by document + +In multi-document sessions, every change in the batch carries the `document_id` it belongs to. Group the review list per document (one sub-section per document, with its own accept-all control) GÇö a flat list of 40 changes spanning three documents is hard to reason about, the same 40 grouped under three document headers is easy. Note that a turn can also **create** a new document even in review mode (creation applies immediately; you'll see it via the `documents_changed` event) GÇö see [SSE Streaming](/guides/streaming#documents_changed). + +### Very large batches + +A whole-document instruction ("tighten every section") can propose hundreds of changes in one batch. Two practical rules from running this at scale: **virtualize the card list** (render only visible rows GÇö hundreds of side-by-side HTML diffs in the DOM at once will lock the tab), and lead with the summary ("312 changes across 5 sections types GÇö Accept all / Review one by one") rather than dropping the user straight into row 1 of 312. + +## Recovering pending approvals after a reload + +A pending review survives your client. If the user reloads the page (or your process restarts) while a job is `awaiting_approval`, the proposal is not lost, and the job keeps blocking new turns on the session until it's resolved (the server auto-denies unattended reviews after about an hour). To restore the review UI on load: + +1. List the session's jobs with `GET /v1/sessions/{session_id}/jobs` (MCP: `get_session_jobs`). +2. Find the job with `status: "awaiting_approval"` and branch on `metadata.awaiting_kind` first: a `continue_prompt` pause is resumed with `/continue`, not `/approve` (see the [flavours warning](#2-poll-for-approval-status) above). +3. Rebuild the review list from `metadata.pending_changes`, exactly as you would from a `proposed_change_batch` event. +4. Skip entries already decided before the reload: `metadata.pending_batch_decisions` maps each decided `change_id` to `{ "approved": true|false, "feedback": ... }`. Render only the undecided remainder, and submit those decisions through the same `/approve` calls as usual. + +## 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: + + + **B2B / server-side integrators (organization `lce_` keys)** that can't hold an `EventSource` open GÇö serverless functions, batch workers, agent runtimes GÇö can long-poll instead of streaming. `GET /v1/poll/{session_id}?since=&timeout=` (org keys only; `timeout` Gëñ 1800s) holds the connection open until there's a job update on the session, then returns it, so you get near-real-time progress without an SSE socket. Pass the last `since` timestamp on each call to avoid duplicate updates. Plain `/v1/jobs/{job_id}` polling (shown above) also works and is the simplest option. + + +```javascript theme={null} +// 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. Every HITL turn GÇö one change or hundreds GÇö +// arrives as a single `proposed_change_batch` event carrying a +// `changes` array (a one-change turn has type "single_approval"). +let pendingChanges = []; + +const showReviewUi = (changes) => { + // Render approve/deny UI here with the full batch. + // When the user decides, call POST /v1/chat/{session_id}/approve + // with the change_id list and the decision. + console.log(`Reviewing ${changes.length} change(s)`); + for (const change of changes) { + console.log(` ${change.change_id}: ${change.operation} GÇö ${change.ai_explanation}`); + } +}; + +es.addEventListener("proposed_change_batch", (event) => { + const data = JSON.parse(event.data); + const batch = JSON.parse(data.content); // { type, batch_id, batch_total, changes } + pendingChanges = batch.changes; + showReviewUi(pendingChanges); +}); + +// Optional safety net for older clients: a standalone `proposed_change` +// listener is harmless but never fires GÇö single changes come through +// `proposed_change_batch` too. You don't need this if starting fresh. + +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 a new `proposed_change_batch` event as the AI tries again. + + + **Apply changes once GÇö either per-accept or from the final, never both.** If your UI applies each approved change to the editor as the user accepts it, do NOT also load the `final` event's full document GÇö it contains those same changes again, and re-applying them duplicates inserted content (newly created sections are the classic symptom). Pick one: incremental apply (ignore the final document) or final-only apply (editor untouched during review). + + + + + **Subscribe to `proposed_change_batch`.** It is the only event that delivers proposed changes GÇö one change or hundreds, it always arrives as a single `proposed_change_batch` event carrying a `changes` array (a one-change turn has `type: "single_approval"`). A standalone `proposed_change` listener never fires; keep one only as a harmless safety net for older clients, but don't make your review flow depend on it. + + +## Complete Python example + +```python theme={null} +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) +``` + + +# Integration Starter Prompt +Source: https://docs.superdocs.app/guides/integration-starter-prompt + +Paste this into your coding agent to integrate SuperDocs into any product, in any stack, with any architecture. Target fill time: under 5 minutes. + +# Integration Starter Prompt + +**Target fill time: under 5 minutes.** Optionally describe what you want SuperDocs to do in your product (1-3 sentences) and paste your existing agent context (`CLAUDE.md`, `AGENTS.md`, `.cursorrules`) or a `package.json`. Anything you leave blank, the agent figures out GÇö it auto-discovers your project root context, runs an investigation framework against your codebase, and (if Part 0 was empty) proposes a plain-English integration plan for you to approve before writing any code. + +## What you'll build + +A complete integration of SuperDocs into your product, regardless of architecture. Your coding agent reads this prompt, reads any context you provide (or auto-discovers from your project root), runs an investigation framework against your product, confirms its understanding with you, and then writes the integration in your stack's idioms. + +This template is built around principles + investigation + cross-references rather than enumerating per-stack scaffolds. It works for any integration shape: + +* **Greenfield** with no app yet GÇö the agent scaffolds one in your chosen stack. +* **Existing app with no editor** GÇö the agent adds SuperDocs surfaced however your UI already works. +* **Existing app with an editor** (ProseMirror, TipTap, Slate, Lexical, Quill, CKEditor, contenteditable) GÇö the agent wires SuperDocs into your existing editor without disrupting it. +* **Existing AI agent** (OpenAI function-calling, Anthropic `tool_use`, LangChain, LlamaIndex) GÇö the agent registers SuperDocs as a tool the AI can call. +* **Server-side worker / batch job / queue processor** GÇö the agent skips all UI work and builds a backend service module. +* **MCP client** (Claude Desktop, Cursor, VS Code, Windsurf) GÇö configuration only, no code to write. +* **Custom architecture you didn't see listed** GÇö the principles + investigation framework are designed to handle this case too. + +## Model selection GÇö pick the right tier per request + +SuperDocs offers four model tiers (`core`, `turbo`, `pro`, `max`) and three thinking depths (`fast`, `balanced`, `deep`). The default is `core` + `balanced` GÇö fast and accurate for everyday edits. + +For document-editing tasks where precision matters (legal contracts, regulatory filings, compliance docs, medical records), pass `model_tier: "pro"` or `model_tier: "max"` in your request body. For batch processing many short edits where speed dominates, use `turbo`. See the full matrix and recommendations at [Model Selection](/guides/model-selection). + +**Important:** Plumb `model_tier` and `thinking_depth` through your integrator's UI GÇö either expose them as request parameters from your end users (e.g. a precision/speed slider, or a "high-stakes mode" toggle), or accept them as configuration. **Do not hard-code them** GÇö different users want different precision/speed tradeoffs, and the right default for a contract-editing app is not the right default for a quick-summarisation app. + +If you skip this step, every request from your product will use the silent `core` + `balanced` default. That's fine for casual editing but will frustrate users on high-stakes documents who need maximum targeting precision. + +## Copy this prompt + +Hit the copy button at the top-right of the code block, then paste into your coding agent (Claude Code, Cursor, Windsurf, Codex, Cline, or any other). The agent will fetch the latest SuperDocs API reference itself GÇö no need to download `llms-full.txt` manually. + +````text theme={null} +# SuperDocs Integration Starter Prompt GÇö Universal Template + +Paste this prompt into your coding agent (Claude Code, Cursor, Windsurf, Codex, Cline, or any other) to integrate SuperDocs into your product. This template is designed to work for any integration shape GÇö a brand-new app, an existing app with an editor, an existing AI agent that needs document-editing capability, a server-side batch worker, an MCP client, or anything else. + +**Target fill time: under 5 minutes.** Almost everything below is optional. The agent reasons from the SuperDocs API docs + your existing context (`CLAUDE.md`, `package.json`, your codebase) and adapts the integration to your product. It will tell you what it inferred before it starts writing code, and you'll have one chance to correct it. + +This template is published at https://docs.superdocs.app/guides/integration-starter-prompt. The filled-in ProseMirror + Next.js reference implementation lives at https://github.com/superdocsapp/prosemirror-superdocs-demo. + +--- + +## Part 0 (recommended) GÇö How do you want SuperDocs in your product? + +In 1-3 sentences, describe what you want SuperDocs to do in your product. This is the strongest single signal the agent uses to determine which integration shape applies. + +**If you fill this in:** the agent goes straight to the investigation framework with a clear target shape. + +**If you leave it blank:** the agent reads your `CLAUDE.md` (or whichever context file is in your project root), infers the most likely integration shape from your codebase, and **proposes a plain-English integration plan for you to approve before writing any code.** You can correct or redirect the proposal GÇö the agent will not start writing code until you confirm the shape. + +Examples (don't copy verbatim GÇö describe YOUR situation): + +- "Brand-new app from scratch with a rich-text editor on the left and a chat panel on the right." +- "Existing Next.js app with a TipTap editor; add SuperDocs as the AI editing engine, surfaced through the editor's existing slash-command menu." +- "Existing AI agent (built with OpenAI function-calling / LangChain / LlamaIndex / Anthropic tool_use) that handles customer queries GÇö add SuperDocs as a tool the agent can call when it needs to edit a document." +- "Backend service that processes contracts overnight via a queue worker GÇö no UI, no human approval, fully automated. Auto-approve all SuperDocs edits." +- "Want my Claude Desktop / Cursor / VS Code IDE to be able to edit my docs via SuperDocs MCP." +- (Anything else GÇö describe the user flow in your product and the agent will figure out where SuperDocs slots in.) + +``` +[describe your situation here in 1-3 sentences GÇö or leave blank to let the agent propose a plan from your CLAUDE.md / codebase] +``` + +--- + +## Part 0A GÇö Existing context (recommended for most teams) + +Most teams already maintain a file that describes their product, stack, and conventions in a form a coding agent reads fluently GÇö typically `CLAUDE.md`, `AGENTS.md`, `.cursorrules`, `.windsurfrules`, or similar in the project root. + +**If you have one of these files in your project root, you do not need to do anything in this section** GÇö the agent will discover and read it automatically as the first step of the protocol below. Skip to running the prompt. + +If your context lives somewhere other than the project root (a Notion page, a Confluence doc, a different filename, an internal brief), use one of the three options below to point the agent at it. + +**Pick one of the three options. You do not need to do all three.** + +### Option 1 GÇö Paste your existing agent context + +If your team has a `CLAUDE.md`, `AGENTS.md`, `.cursorrules`, internal developer brief, or equivalent, paste its contents between the markers below. The coding agent will infer the Part 0B declarations from this context. + +``` + +``` + +### Option 2 GÇö Link to existing internal docs + +If your context lives in Notion, Confluence, a GitHub README, or a public docs site, list the URLs below. The coding agent will fetch and read them before proceeding. + +- +- + +### Option 3 GÇö Paste your package manifest + one representative file + +If you don't have agent context yet but have an existing codebase, paste your package manifest (`package.json`, `requirements.txt`, `Gemfile`, `go.mod`, `pyproject.toml`, etc.) and the path to one file that handles documents today GÇö the current editor component, the current upload endpoint, whatever is closest to where SuperDocs will plug in. + +``` + +``` + +Representative file path(s): + +- + +### If none apply + +Skip Part 0A entirely. The agent will still auto-discover any context file in your project root and infer remaining values from your codebase once it starts reading files. + +--- + +## Part 0B GÇö Optional declarations + +Fill in whatever you already know. Leave the rest blank GÇö the coding agent will read your codebase and infer each missing value, then echo its inferences back before writing code. **Do not fill these in defensively.** Agents that have read your actual `package.json`, editor component, and framework files produce better picks than blanket defaults. + +### Descriptive slots + +- **EXISTING_APP_CONTEXT:** _(describe the codebase GÇö only relevant if this is not a greenfield build. Leave blank to let the agent read the repo itself.)_ +- **DOC_FLOW_DESCRIPTION:** _(how documents currently enter, live in, and leave the product. Leave blank to let the agent infer from the codebase.)_ +- **SAMPLE_DOCUMENT:** _(starting HTML for the demo's first edit. Leave blank to let the agent generate or pick one from your existing data.)_ +- **EXISTING_EDITOR_FILE_PATH:** _(if you have a rich-text editor, the path to its component file. Leave blank GÇö the agent will find it via file search.)_ + +### Scenario axes GÇö all optional + +Fill in a value if you have a firm preference. Leave blank to let the agent choose based on your codebase GÇö it will echo back its pick before starting. + +- **APP_MATURITY:** `greenfield` -+ `existing-no-editor` -+ `existing-with-editor` -+ `existing-with-ai-agent` _(leave blank to detect from the repo.)_ +- **EDITOR_FRAMEWORK:** `prosemirror` -+ `tiptap` -+ `slate` -+ `lexical` -+ `quill` -+ `ckeditor` -+ `contenteditable` -+ `none` -+ `other` _(leave blank to detect from `package.json`.)_ +- **AUTH_STATE:** `none-demo-only` -+ `firebase` -+ `auth0` -+ `clerk` -+ `custom-sessions` -+ `enterprise-sso` _(leave blank to detect.)_ +- **BACKEND_STATE:** `next-api-routes` -+ `express` -+ `fastapi` -+ `rails` -+ `django` -+ `go` -+ `dotnet` -+ `serverless` -+ `other` _(leave blank to detect from project layout.)_ +- **LANGUAGE:** `typescript` -+ `javascript` -+ `python` -+ `go` -+ `ruby` -+ `java` -+ `csharp` -+ `php` _(leave blank to detect from file extensions.)_ +- **FRONTEND_FRAMEWORK:** `nextjs` -+ `remix` -+ `vite-react` -+ `sveltekit` -+ `vue` -+ `plain-html` -+ `rails-view` -+ `django-template` -+ `htmx` -+ `desktop` -+ `none` (server-only) _(leave blank to detect.)_ +- **DEPLOYMENT_TARGET:** `localhost-demo` -+ `browser-saas` -+ `server-only-no-ui` -+ `desktop-app` -+ `mcp-only` -+ `agent-tool` _(leave blank GÇö Part 0 usually answers this.)_ +- **APPROVAL_MODE:** `human-via-ui` -+ `ai-decides` -+ `auto-approve-all` -+ `human-out-of-band` (Slack, email) _(leave blank GÇö Part 0 usually answers this.)_ +- **API_KEY_STORAGE_PREFERENCE:** `env-var` -+ `secrets-manager` -+ `vault` -+ `existing-config-file` -+ `other` _(leave blank to detect from your existing secret-handling patterns.)_ + +--- + +## Agent protocol GÇö read, infer, echo back, ask, wait + +Before executing **any** part of the integration, the coding agent must: + +1. **READ CONTEXT.** Before doing anything else: + a. Search the project root for any context files the integrator may have already maintained. Look for, in this order: `CLAUDE.md`, `AGENTS.md`, `.cursorrules`, `.windsurfrules`, `.aiderrules`, `GEMINI.md`, `COPILOT.md`, `INSTRUCTIONS.md`, or any file matching the pattern `*RULES*.md` / `*AGENT*.md` / `*INSTRUCTIONS*.md` in the project root. If any of these exist, read all of them GÇö these files describe the integrator's product, stack, and conventions and are higher-priority context than anything else. Do NOT assume your coding-agent harness auto-loaded them; read them explicitly via your file-read tool so you know what's in them. + b. Read every file and URL provided in Part 0A (paste-in context, links to internal docs, package manifests). + c. Read the integrator's existing codebase GÇö at minimum, the package manifest (`package.json`, `requirements.txt`, `Gemfile`, `go.mod`, `pyproject.toml`, etc.), the main framework config files, any editor component or AI-agent file if declared or discoverable, and the `README` if present. + d. **Fetch the SuperDocs API reference fresh from `https://docs.superdocs.app/llms-full.txt`** GÇö even if a stale local copy exists. Run `curl -sS https://docs.superdocs.app/llms-full.txt -o llms-full.txt` to grab the current version. The live URL is always authoritative; the local copy may be days or weeks old. +2. **INFER.** For every Part 0B slot the integrator left blank, infer the value from context. If a slot cannot be inferred with reasonable confidence, mark it as `UNSET`. +3. **RUN THE INVESTIGATION FRAMEWORK** in Part 4 -ºC below. Answer all five questions about the integrator's product. If you can't answer a question from available context, mark it `UNSET GÇö asking you`. +4. **ECHO BACK.** Present every Part 0B value AND every Part 4 -ºC answer with a source tag: + - `[provided]` GÇö the integrator explicitly set this in Part 0 / Part 0B / Part 0A. + - `[inferred from ]` GÇö name the source. + - `[UNSET GÇö asking you]` GÇö genuinely ambiguous. +5. **ASK.** For each `UNSET` slot, ask the integrator specifically, offering 2GÇô3 suggested options. +6. **PROPOSE A PLAN (only if Part 0 was left blank).** If the integrator did not describe their integration goal in Part 0, synthesize a 1-paragraph plain-English integration proposal from your -ºC answers and Part 0B inferences. Frame it as: *"Here's what I understood from your codebase. I propose to build [X], surfaced in [Y], with [Z] approval mechanism. Confirm or correct this before I start writing code."* Do NOT proceed until the integrator confirms the proposed shape. +7. **WAIT.** Do not start writing code until the integrator has confirmed the inferred values, answered any `UNSET` questions, and (if applicable) approved the proposed integration plan. + +This prevents the agent from confidently integrating into the wrong shape because it misread pasted context or guessed at an ambiguous axis. + +--- + +## Last-resort fallback (only when no signal at all) + +If the agent cannot infer anything from context AND the integrator has left Part 0 blank AND cannot answer follow-up questions, the agent may proceed with the following blanket fallback GÇö **but must announce it clearly before starting:** + +> *"I had no context to work with, so I'm defaulting to the fastest-to-ship demo stack below. Tell me to stop if this isn't what you want."* + +- **Integration shape:** greenfield UI build (editor + chat panel) +- **LANGUAGE:** `typescript` +- **FRONTEND_FRAMEWORK:** `nextjs` +- **EDITOR_FRAMEWORK:** `tiptap` (lower chunk-id-schema friction than raw ProseMirror for a blind start) +- **BACKEND_STATE:** `next-api-routes` +- **AUTH_STATE:** `none-demo-only` +- **APPROVAL_MODE:** `human-via-ui` +- **API_KEY_STORAGE_PREFERENCE:** `env-var` +- **DEPLOYMENT_TARGET:** `localhost-demo` + +This fallback is for the genuinely-no-signal case only. Inference from any real context GÇö package manifest, existing editor or AI-agent files, repo layout, customer's Part 0 description GÇö always beats this blanket stack. + +--- + +## Dry-run checklist (verify before declaring the integration done) + +Whatever shape your integration takes, verify these five universal checks: + +1. **Document HTML round-trip preserves `data-chunk-id` attributes.** Send a real document HTML to SuperDocs, receive the response, send the response back unchanged on the next request, verify the `chunk_id` references in `proposed_change_batch` events match elements that exist in your document. +2. **API key is server-side only.** No browser network request should contain the `sk_` value. Verify in DevTools or your backend logs. +3. **At least one full round-trip succeeds end-to-end.** A user (or your AI, or your batch worker) sends a message; SuperDocs returns proposed changes; those changes are surfaced (UI / agent / log) per your declared `APPROVAL_MODE`; approved changes apply to the document. +4. **Failure modes are handled.** If the SuperDocs API returns 4xx/5xx, your code surfaces a useful error. If the SSE stream drops, your code reconnects or surfaces an error. +5. **Your integration matches your product's existing idioms.** Don't introduce a new state library, new component framework, or new HTTP client just for SuperDocs. Match what your codebase already uses. + +--- + +## Part 1 GÇö Mandatory context + +Read these before writing code: + +1. **The full SuperDocs API reference at `https://docs.superdocs.app/llms-full.txt`.** Fetch the latest version yourself GÇö do not assume a local copy is current: + ```bash + curl -sS https://docs.superdocs.app/llms-full.txt -o llms-full.txt + ``` + Run this even if a stale `llms-full.txt` already exists in the project folder; the API surface evolves and the live URL is always authoritative. Refer to this file whenever an API question comes up. +2. This prompt, Parts 2GÇô8. +3. Your own codebase GÇö read enough of `EXISTING_APP_CONTEXT` paths to understand where documents live, where your authenticated backend routes live, and how your frontend (or your AI agent) consumes data. + +Do not search the internet for integration patterns beyond these sources. Everything you need is in `llms-full.txt`, in this template, or in the linked docs guides. + +--- + +## Part 2 GÇö Hard constraints + +### Must always be true of any SuperDocs integration + +- The integration produces a working "send document GåÆ receive proposed changes GåÆ apply approved changes" loop appropriate to the product's existing patterns. +- The SuperDocs `sk_` API key is never exposed to a browser, mobile app, or any client-side surface. +- The document HTML (with `data-chunk-id` attributes) round-trips cleanly between wherever the document lives and SuperDocs. +- The approval mechanism (human-via-UI, AI-decides, auto-approve, human-out-of-band) matches what `APPROVAL_MODE` declares (or what Part 0 implies). + +### Stack GÇö match the customer's existing system + +Do NOT impose Next.js, ProseMirror, Tailwind, or any other specific stack on a project that isn't already using it. If the customer's codebase is Rails + Slate + plain CSS, the integration is Rails + Slate + plain CSS. The reference implementation in the GitHub repo uses Next.js + ProseMirror + Tailwind because that's what the demo video builds GÇö your job is to adapt the patterns to the customer's actual stack, not to scaffold a duplicate stack alongside theirs. + +### Out of scope for a first integration + +- Attachments, image uploads, and templates. All exist in the API; they're separate integrations. +- Multi-document UI, session list, user impersonation. Not part of the API integration. + +### Content safety GÇö what to never include in your code, comments, or public-facing artefacts + +- Specific AI model names (the SuperDocs API includes the model; your users do not need to know which one). +- Third-party orchestration framework names that SuperDocs might use internally. +- Cloud provider names beyond "hosted" or "server-side." +- Database technology assumptions about SuperDocs' own backend. +- Cost-per-operation numbers or internal SuperDocs pricing. + +Say "SuperDocs' AI" or "the AI" when you need to refer to the model. + +--- + +## Part 3 GÇö Standing rules during the build + +1. **Strong types.** Whatever your language, use the strictest practical type mode. No `any`, no `interface{}`, no untyped dicts at boundaries. +2. **No decorative error handling.** If an HTTP call fails, let it surface to a toast / log / return value GÇö don't paper it over with silent catches. +3. **Comments only where the *why* is non-obvious.** Do not narrate what the code does. +4. **Commit after every phase** (locally; don't push until the integration works end-to-end). If something breaks in Phase N+1, `git reset` is one keystroke away. +5. **Read `llms-full.txt` whenever an API question comes up.** It is always the answer faster than guessing. +6. **Match the customer's idioms.** If their codebase uses Express middleware patterns, write Express middleware. If it uses class-based services, write a class-based service. Don't introduce new patterns. + +--- + +## Part 4 GÇö Universal Integration Framework + +This Part replaces the per-shape phase trees you might expect from a typical integration guide. SuperDocs deliberately reasons from principles rather than enumerating every possible customer architecture, because customer systems are too varied for a fixed set of templates. The framework below works for every shape GÇö UI build, AI-agent tool, server-side worker, MCP client, embedded-in-existing-editor, custom GÇö by giving the agent everything it needs to reason from your specific product. + +### -ºA GÇö Integration principles + +What SuperDocs requires from any integration, regardless of shape: + +1. **Documents are HTML in / HTML out.** SuperDocs accepts an HTML document and returns an HTML document. There is no proprietary format GÇö you send and receive standard HTML strings. +2. **`data-chunk-id` attributes on block-level elements must round-trip cleanly.** SuperDocs returns HTML with `data-chunk-id=""` attributes on paragraphs, headings, lists, blockquotes, code blocks, and other block-level elements. Whatever holds your document (rich-text editor, database row, in-memory string, file storage) must preserve these attributes when the document goes back to SuperDocs on the next request. Failure mode is silent: surgical editing intermittently fails on chunks whose attribute was stripped. +3. **The integration loop is always the same shape.** Send document HTML + a message GåÆ receive proposed changes (one or many) GåÆ decide which to apply (human, AI, or auto-approve) GåÆ apply approved changes GåÆ persist the resulting HTML back to wherever the document lives. +4. **Approval can take many shapes.** A human reviewing a UI diff card. A human notified out-of-band (Slack, email) who replies with approval. Your AI agent making the decision based on its own reasoning. Auto-approve-all for batch processing where no review is wanted. +5. **Streaming (SSE) is one option, not the only option.** For interactive user-facing flows, SSE gives the best UX (progress events, mid-flight diffs). For server-side batch processing or AI-agent-tool integration, the synchronous `/v1/chat` endpoint or polling on `/v1/jobs/{id}` are simpler and equally valid. +6. **Your existing system's idioms are the right idioms.** SuperDocs is one piece of plumbing among many in your product. The integration code should look like the rest of your codebase GÇö same HTTP client, same error handling, same logging conventions, same component patterns. + +### -ºB GÇö Canonical minimal example (the loop in 15 lines) + +Whatever shape your integration ends up taking, this is the conceptual core. Every richer integration is a wrapper around this loop: + +```bash +# Set your key once +export SUPERDOCS_API_KEY="sk_YOUR_KEY" + +# One round-trip: send a document + a message, get the edited document back. +curl -sS -X POST https://api.superdocs.app/v1/chat \ + -H "Authorization: Bearer $SUPERDOCS_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "message": "Make the introduction one sentence shorter.", + "session_id": "demo-1", + "document_html": "

Welcome

Hello from our application. We are glad you are here.

", + "approval_mode": "approve_all" + }' \ +| jq -r '.document_changes.updated_html' +``` + +Output: the updated HTML, ready to render or persist back to wherever your document lives. + +That's the whole loop. Every integration shape GÇö editor + chat, AI-agent tool, server-side worker, MCP client GÇö is a richer version of this same call: + +- An editor + chat UI wraps the call in a frontend that sends the editor's HTML, streams progress via SSE, and applies the result back to the editor via a `setHtml()` bridge. +- An AI agent wraps the call in a tool function that the agent invokes when it decides a document edit is needed. +- A server-side worker iterates a queue, calling this loop synchronously per document, persisting the result to storage. +- An MCP client connects to `https://api.superdocs.app/mcp/` and lets a meta-agent (Claude Desktop, Cursor) trigger the same call. + +The SSE variant adds streaming progress and human-in-the-loop review. The polling variant adds the ability to fire-and-forget for long jobs. But the principle is unchanged: HTML in, HTML out, with optional approval in the middle. + +### -ºC GÇö Investigation framework + +Before writing any integration code, you (the agent) MUST answer these five questions about the integrator's product. Output a short decision document (5-10 lines) and present it for the integrator's confirmation before proceeding to -ºD. + +1. **Where does the document HTML come from?** (rich-text editor in a browser / file storage like S3 or local disk / database row / API endpoint / generated by another AI step / pasted by a user / streamed in from another system) +2. **Who decides whether a proposed change is applied?** (a human via UI / your AI agent making the decision based on its own reasoning / auto-approve all changes / a human notified out-of-band, e.g. via Slack or email) +3. **Where does the approved HTML go?** (back into the same editor / persisted to storage / pushed to another system / returned via API to a caller / written to a file) +4. **What's your product's idiom for handling streaming or async events?** (web SSE / WebSocket / polling / queue worker / synchronous request-response / message bus) +5. **Does any pattern in your existing system look similar to what SuperDocs is being added to do?** (existing rich-text edit flow, existing AI suggestion flow, existing document workflow, existing content moderation, existing review queue, etc.) Match the new integration to the existing idiom GÇö don't invent a new pattern. + +If a question can't be answered from the available context, mark it `UNSET GÇö asking you` and ASK the integrator before proceeding. Do not invent answers. + +### -ºD GÇö Universal 5-phase tree + +These five phases apply regardless of integration shape. Each phase has a one-paragraph description of WHAT to build (not how) and a pointer to the relevant docs page for the HOW. + +#### Phase 1 GÇö Investigate and confirm + +Run the -ºC investigation framework. Output a short decision document covering all five questions plus the Part 0B declarations. Present it to the integrator for confirmation. Do not proceed to Phase 2 until confirmed. + +#### Phase 2 GÇö Set up the API client + +Create a server-side wrapper around the SuperDocs API in a module appropriate to the customer's stack GÇö a Next.js API route, an Express middleware, a FastAPI endpoint, a Rails controller, a Go handler, a .NET minimal-API endpoint, etc. The `sk_` API key never reaches a browser, mobile app, or any client-side surface; it lives in the backend's environment / secret store. + +GåÆ For the Next.js / Express / FastAPI / Rails / Django / Go / .NET API-key proxy patterns: **https://docs.superdocs.app/guides/streaming** (the same guide also covers SSE auth setup with the `api_key` query param required by `EventSource`). + +GåÆ For server-side-only integrations (no browser, no proxy required): **https://docs.superdocs.app/guides/server-integration**. + +#### Phase 3 GÇö Implement the document round-trip + +Build the "send HTML GåÆ receive proposed changes" loop in your customer's idiom. The shape varies: + +- If the customer's product has a rich-text editor: wire the editor to `getHtml()` / `setHtml()` bridges, send via the API client from Phase 2, render proposed changes via the approval mechanism in Phase 4. +- If the customer's product has an AI agent: register SuperDocs as a tool the agent can invoke, with the tool function calling the API client from Phase 2 and returning structured proposed-change data the agent can reason about. +- If the customer's product is a server-side worker: pull documents from your queue / storage, call the API client from Phase 2 with `approval_mode: "approve_all"`, persist the returned HTML back to storage. +- If the customer's product is an MCP client: configure the client to connect to `https://api.superdocs.app/mcp/`. There is no code to write GÇö it's a configuration step. + +GåÆ For editor integrations (preserving `data-chunk-id` round-trip in ProseMirror, TipTap, Slate, Lexical, Quill, CKEditor, or contenteditable): **https://docs.superdocs.app/guides/editor-integration**. + +GåÆ For AI agent tool integrations (registering SuperDocs as a tool in OpenAI function-calling, Anthropic `tool_use`, LangChain, LlamaIndex, or any other tool-using framework): **https://docs.superdocs.app/guides/agent-tool-integration**. + +GåÆ For server-side / batch / queue-worker integrations (sync API, polling, `approval_mode: "approve_all"`): **https://docs.superdocs.app/guides/server-integration**. + +GåÆ For MCP clients (Claude Desktop, Cursor, VS Code, Windsurf, Claude Code): **https://docs.superdocs.app/mcp/setup**. + +#### Phase 4 GÇö Implement the approval mechanism + +Build the appropriate approve/deny flow based on -ºC question 2: + +- **`human-via-ui`** GÇö render proposed changes as inline diff overlays in the editor, side-by-side cards in a chat panel, modals, notification toasts, or whatever pattern your UI already uses. On approve, POST to `/v1/chat/{session_id}/approve`. +- **`ai-decides`** GÇö your AI agent receives the proposed change as structured data (chunk_id, old_html, new_html, ai_explanation) and decides whether to approve based on its own reasoning. Same approve POST, but the decision is made by the agent. +- **`auto-approve-all`** GÇö pass `approval_mode: "approve_all"` in the initial request. SuperDocs applies all changes without pausing for review. No second API call needed. +- **`human-out-of-band`** GÇö your service sends a notification (Slack, email, SMS) with the proposed change, waits for the human's response (button click, reply, etc.), then POSTs to `/v1/chat/{session_id}/approve`. + +GåÆ For human UI approval patterns (inline diff overlay, side-by-side cards, modals, etc.): **https://docs.superdocs.app/guides/human-in-the-loop**. + +GåÆ For AI-driven approval inside an agent's reasoning loop: **https://docs.superdocs.app/guides/agent-tool-integration**. + +#### Phase 5 GÇö Test end-to-end + +Run a real document through the full loop. Verify the dry-run checklist above. Verify the customer's actual product (whatever it looks like) shows the expected outcome GÇö an editor that updates in place, an AI agent that completes its reasoning with the document edited, a queue worker that produces edited documents in storage, etc. + +### -ºE GÇö Per-pattern cross-references + +This table maps common implementation choices to the right docs page. Use it to find the snippet that matches your customer's product: + +| If your product has... | Look here for the implementation pattern | +|---|---| +| A rich-text editor (ProseMirror, TipTap, Slate, Lexical, Quill, CKEditor, contenteditable, custom) | [Editor Integration](https://docs.superdocs.app/guides/editor-integration) | +| An existing AI agent (OpenAI function-calling, Anthropic `tool_use`, LangChain, LlamaIndex, generic JSON-schema tool) | [Agent Tool Integration](https://docs.superdocs.app/guides/agent-tool-integration) | +| A backend service / queue worker / batch job (Node, Python, Go, Ruby, .NET, etc.) with no UI | [Server Integration](https://docs.superdocs.app/guides/server-integration) | +| HITL approval rendered in your UI | [Human-in-the-Loop](https://docs.superdocs.app/guides/human-in-the-loop) | +| Real-time streaming (SSE) for progress events | [SSE Streaming](https://docs.superdocs.app/guides/streaming) | +| Long-running async jobs with polling | [Async Jobs](https://docs.superdocs.app/guides/async-jobs) | +| MCP client integration (Claude Desktop, Cursor, VS Code, Windsurf) | [MCP Setup](https://docs.superdocs.app/mcp/setup) | +| Working code in a specific language | [JavaScript](https://docs.superdocs.app/examples/javascript), [Python](https://docs.superdocs.app/examples/python), [curl](https://docs.superdocs.app/examples/curl) | + +### -ºF GÇö Stuck? + +If your integration shape isn't listed in -ºE and the principles in -ºA don't obviously map to your product, email `hello@superdocs.app` or book a 15-minute integration call at `https://cal.com/superdocs`. We'll talk through the pattern and add a snippet to the docs for the next person. + +--- + +## Part 5 GÇö API contract summary + +All requests go through your API client (Phase 2). Paths are relative to `https://api.superdocs.app/`. + +- `POST v1/chat` GÇö sync: body `{ message, session_id, document_html, approval_mode?: "approve_all" | "ask_every_time", document_id? }` GåÆ returns the full result inline. Best for batch / server-side / agent-tool integrations that don't need streaming. `document_id` targets a specific open document in multi-document sessions (omit = focused document). +- `POST v1/chat/async` GÇö async: body same as above GåÆ returns `{ job_id }`. Use with the SSE stream or polling endpoint. +- `GET v1/chat/{session_id}/stream?job_id=...&api_key=...&last_sequence=...` GÇö SSE stream of nine event types (`intermediate`, `proposed_change_batch`, `document_sync`, `continue_prompt`, `documents_changed`, `model_fallback`, `final`, `usage`, `error`). `proposed_change_batch` is the event that carries proposed changes for human review (its `changes[]` array holds the whole batch GÇö single-change turns arrive as a one-element array). Browser-side requires the `api_key` query param because `EventSource` can't set custom headers; on reconnect pass `last_sequence` to replay only newer events. +- `GET v1/jobs/{job_id}` GÇö poll for async job status. Use as an alternative to SSE for non-browser consumers. +- `POST v1/chat/{session_id}/approve` GÇö body: `{ job_id, approved, changes?: [{ change_id, approved, feedback? }] }`. Resumes the AI on the already-open SSE stream. +- `GET v1/sessions/{session_id}/documents` GÇö list the session's open documents (multi-document sessions): ids, chunk counts, focused flag, HTML. `POST .../documents/{document_id}/focus` switches focus; `DELETE .../documents/{document_id}?next_focus=...` closes one. +- `POST v1/sessions/{session_id}/revert` GÇö body: `{ turn_index }` GåÆ returns `{ compose_text, reverted_to_turn, document_state, editor_action, archived_turn_count }`. Rewinds chat + document to the state before the user message at `turn_index`. Returns `409` if a chat job is in flight on the session, `422` if the target message predates the revert feature. +- `POST v1/documents/export` GÇö body: `{ html?, session_id?, upload_id?, format: "docx" | "pdf" | "html" | "markdown" | "txt", options?, filename? }` GåÆ binary file (`docx` default). Non-fatal issues arrive in the `X-Export-Warnings` response header. + +SSE event `data:` shapes (all JSON): + +``` +document_sync { type, content } // content: full HTML +intermediate { type, content, sequence, timestamp } +proposed_change_batch { type, content, sequence } // content: JSON-stringified batch (changes[]) +final { content, result } // result.response, result.document_changes.updated_html +usage { monthly_used, monthly_limit, monthly_remaining, was_billable, subscription_tier } +error { error } +``` + +`proposed_change_batch` is the event that carries proposed changes for human review. The whole turn arrives as **one** event whose `changes[]` array holds every proposed change GÇö even a single-change turn arrives as a one-element array. (You may also register a `proposed_change` listener defensively for forward-compatibility, but it never fires today GÇö `proposed_change_batch` is what's emitted, so build your approval loop on it.) + +`proposed_change_batch.content` is a JSON-stringified string GÇö you must `JSON.parse` it twice to access the batch. See the warning callout in the [SSE Streaming guide](https://docs.superdocs.app/guides/streaming). + +Parsed `proposed_change_batch.content` shape: +``` +{ batch_id, batch_total, + changes: [ + { change_id, operation: "edit" | "create" | "delete", + chunk_id, old_html, new_html, + ai_explanation, document_id, insert_after_chunk_id }, + ... + ] } +``` + +Iterate `changes[]` to render each proposed change; call `/approve` with the `change_id` of each. + +--- + +## Part 6 GÇö Failure modes and their fixes + +| Symptom | Likely cause | Fix | +|---|---|---| +| `Missing SUPERDOCS_API_KEY` | env file not read | Restart your server; env vars are read on boot. | +| 401 from any endpoint | key wrong or includes `Bearer ` literal | Value is just `sk_...` with no quotes, no "Bearer" prefix. | +| SSE connection opens then closes | proxy transforms or buffers headers | Pipe upstream body unchanged; set `Cache-Control: no-cache, no-transform`. Disable buffering at any reverse proxy (nginx, Caddy, CloudFront). | +| `proposed_change_batch` fires, approve POST returns 404 | wrong session_id in the URL | Path must include the session ID exactly as sent in the original `/v1/chat/async` request. | +| Revert returns 409 | A chat job is in flight on the session | Wait for the SSE `final` event (or poll `/v1/jobs/{job_id}` until status is `completed`/`failed`) before retrying revert. Optionally cancel the job via `POST /v1/jobs/{job_id}/cancel` first. | +| Revert returns 422 | The target user message predates the revert feature | Per-message revert only works on chats started after the feature shipped. Older sessions stay readable but their messages don't carry the rewind marker. Start a new session for full revert support. | +| Document loses `data-chunk-id` after first edit | editor schema/config missing a node type | Add the missing node to your chunk-id-preservation config (see [Editor Integration](https://docs.superdocs.app/guides/editor-integration)). | +| Diff card has empty `old_html` | operation is `create`, not `edit` | Correct GÇö creates have no `old_html`. Render them as "new section" cards. | +| `proposed_change_batch.content` fields all undefined | missed the JSON double-parse | `JSON.parse(JSON.parse(event.data).content)` to access the batch, then iterate its `changes[]`. | +| Silent round-trip loss of attributes | HTML sanitizer strips unknown attributes | Add `data-chunk-id` to your sanitizer's allowlist. | +| AI agent's tool call fails to parse the response | agent expects JSON but SuperDocs returned HTML in the `updated_html` field | The HTML is the value; pass it through to wherever your document lives. The agent should treat it as opaque content, not parse it. | + +--- + +## Part 7 GÇö Self-review checklist + +Verify all applicable items before declaring the integration done. Skip items gated on scenarios you didn't enable. + +- [ ] You answered all five -ºC investigation questions and got integrator confirmation. +- [ ] A user message (or your agent's tool call, or your worker's queue item) reaches SuperDocs with the current document HTML attached. +- [ ] At least one full round-trip succeeds end-to-end and produces a visible result in the customer's actual product. +- [ ] The SuperDocs `sk_` key never appears in any browser network request, mobile app log, or other client-side surface. +- [ ] The `data-chunk-id` attribute survives the round-trip on every block node type your product uses (verify with a real document, not a synthetic one). +- [ ] If using SSE streaming or `approval_mode="ask_every_time"` (or any operation that may take >10 seconds), `intermediate` progress events are rendered to the user as in-flight chat bubbles or a progress indicator. The user sees text updates arriving every few seconds GÇö never a silent screen for 30+ seconds. Long operations on large documents can take minutes; a frozen UI looks identical to a crashed one. See [Streaming GåÆ Rendering intermediate events in your UI](/guides/streaming#rendering-intermediate-events-in-your-ui). +- [ ] Your client validates that `document_html` is non-empty before sending. Editor refs can be transiently `null` during framework remounts (React Strict Mode, HMR, Fast Refresh) and silently return empty strings GÇö cache the last successful `document_html` (from the previous `final.updated_html` or your editor's confirmed state) and fall back to it before sending. +- [ ] The approval mechanism matches what `APPROVAL_MODE` declared (human-via-UI / AI-decides / auto-approve / human-out-of-band). +- [ ] A second message in the same session continues the conversation (the AI refers back to the prior edit). +- [ ] Failure paths are handled GÇö bad API responses surface useful errors; dropped streams reconnect or surface errors. +- [ ] The integration matches your product's existing idioms (HTTP client, error handling, component patterns) GÇö no foreign-looking code. + +--- + +## Part 8 GÇö Content safety (last reminder) + +Nothing in your code, comments, commit messages, or public artefacts mentions specific AI model names, orchestration framework names, internal SuperDocs tech names, cloud providers, or cost-per-operation figures. Say "SuperDocs' AI" or "the AI." + +--- + +## You are now starting + +Confirm with the integrator that the Part 0 description, Part 0B inferences, and -ºC investigation answers are correct, then execute Phases 1GÇô5 in order. Read `llms-full.txt` and the linked docs guides whenever an API question or implementation question comes up. Commit locally after every phase. Do not push until the end-to-end integration works. + +Good luck. +```` + +## Reference implementation + + + + Live build of this exact integration end-to-end with an on-screen timer GÇö raw ProseMirror on Next.js wired to the SuperDocs REST API, ending with the full SOP-editing demo. Less than 15 minutes of actual coding. + + + + The filled-in repo from the video GÇö raw ProseMirror on Next.js with the full SOP-editing demo. MIT-licensed. Clone it, point your coding agent at it, and adapt. This is one specific shape of integration; the prompt above adapts to your product's actual shape. + + + +## Related guides + +* [Editor Integration](/guides/editor-integration) GÇö drop-in chunk-id preservation snippets for ProseMirror, TipTap, Slate, Lexical, Quill, and CKEditor 5 (plus a "for other editors" generic guide). +* [Agent Tool Integration](/guides/agent-tool-integration) GÇö register SuperDocs as a tool in your existing AI agent's tool registry (OpenAI function-calling, Anthropic `tool_use`, LangChain, LlamaIndex, generic JSON-schema). +* [Server Integration](/guides/server-integration) GÇö backend service / batch worker / queue processor patterns in Node, Python, Go, Ruby, and .NET. +* [Human-in-the-Loop](/guides/human-in-the-loop#rendering-diffs-inline-in-your-editor) GÇö inline diff overlay patterns (Cursor-style) and side-by-side review cards. +* [SSE Streaming](/guides/streaming) GÇö nine event types, the `proposed_change_batch.content` double-parse, and the `EventSource` + `api_key` query-param pattern. +* [Async Jobs](/guides/async-jobs) GÇö long-running operations with polling instead of SSE. +* [JavaScript Examples](/examples/javascript) GÇö working HITL flow plus a framework-agnostic two-pane frontend skeleton. +* [MCP Setup](/mcp/setup) GÇö for Claude Desktop, Cursor, VS Code, Windsurf, and other MCP clients. + +## Stuck? + +Email [hello@superdocs.app](mailto:hello@superdocs.app) or book a 15-minute integration call at [cal.com/superdocs](https://cal.com/superdocs). If your integration shape isn't covered by the existing guides, we'll add a snippet for the next person. + + +# Model Selection +Source: https://docs.superdocs.app/guides/model-selection + +Choose from four AI model tiers and three thinking depths to balance speed, capability, and cost. + +# Model Selection + +SuperDocs offers four model tiers and three thinking depths. All tiers are available on every plan. + +## Model tiers + +Set `model_tier` in your request to choose a model: + +| Tier | Best for | Speed | Thinking depth control | +| ------- | ------------------------------------ | -------- | ---------------------- | +| `core` | Everyday editing, quick tasks | Fast | Yes | +| `turbo` | Speed-critical workflows | Fastest | Yes | +| `pro` | Complex analysis, multi-step edits | Moderate | Yes | +| `max` | Challenging documents, nuanced tasks | Slower | Yes | + +Default: `core` + +## Thinking depth + +Set `thinking_depth` to control how much reasoning the AI applies: + +| Depth | Behavior | +| ---------- | ------------------------------------------ | +| `fast` | Quick responses, minimal reasoning | +| `balanced` | AI decides when to reason deeply (default) | +| `deep` | Extended reasoning for complex problems | + +Default: `balanced` + + + `thinking_depth` is honored on **all four tiers**. Each tier maps the three depths to reasoning budgets tuned for that tier, so `deep` on `turbo` and `deep` on `max` both mean "reason harder than this tier's default" GÇö not identical budgets. If you previously skipped `thinking_depth` on `turbo`/`pro` (it used to be ignored there), it now takes effect; the default `balanced` keeps prior behavior. + + +## Usage + +```bash theme={null} +# Core with custom thinking depth +curl -X POST https://api.superdocs.app/v1/chat \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "message": "Analyze this contract for potential risks", + "session_id": "my-session", + "document_html": "...", + "model_tier": "core", + "thinking_depth": "deep" + }' + +# Pro with deep reasoning for a high-stakes pass +curl -X POST https://api.superdocs.app/v1/chat \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "message": "Analyze this contract for potential risks", + "session_id": "my-session", + "document_html": "...", + "model_tier": "pro", + "thinking_depth": "deep" + }' +``` + +## Recommendations + +| Task | Suggested tier | Suggested depth | +| ------------------------------- | --------------- | --------------- | +| Fix a typo | `core` | `fast` | +| Rewrite a paragraph | `core` | `balanced` | +| Draft a multi-section document | `core` or `max` | `deep` | +| Analyze a complex contract | `pro` or `max` | `deep` | +| Batch processing many documents | `turbo` | GÇö | + +## Token cost expectations by operation type + +Use this as a planning guide for how many output tokens GÇö and roughly how much money GÇö a given operation will spend. + +### Typical per-operation output token usage + +| Operation | Sections involved | Expected output tokens | Approx. cost (default tier) | +| ---------------------------------------------- | ----------------- | ---------------------- | --------------------------- | +| Single-section edit (typo, reword, format) | 1 | 5,000 GÇô 20,000 | $0.001 GÇô $0.004 | +| Multi-section edit (3GÇô5 sections) | 3GÇô5 | 20,000 GÇô 80,000 | $0.004 GÇô $0.016 | +| Full-document operation (rewrite, restructure) | 10GÇô20 | 80,000 GÇô 250,000 | $0.016 GÇô $0.050 | + +Anything materially above these ranges (e.g. a single-section edit using 100,000+ output tokens) suggests a runaway plan or an unusually large section. Inspect the operation in your usage dashboard if you see this GÇö and feel free to flag it to us, since it's also useful as a signal for our own quality monitoring. + +### Reasoning tokens are additive + +Each thinking depth adds reasoning tokens on top of the operation's output: + +| Thinking depth | Approx. reasoning tokens added | Latency added | +| -------------------- | --------------------------------- | ------------- | +| `fast` | up to 2,000 | \< 1 s | +| `balanced` (default) | dynamic GÇö typically 4,000 GÇô 8,000 | 1 GÇô 3 s | +| `deep` | up to 16,000 | 3 GÇô 8 s | + +Reasoning tokens are billed at the model's standard output rate, so on the default tier a `deep` reasoning addition contributes \< \$0.005 per request even at the upper end. + +### Picking a tier for batch jobs + +If you're processing 100+ documents in a batch: + +* **Cost-first** GÇö `model_tier: "turbo"`. Fastest, lowest cost, slight precision tradeoff. Good for analytics-style passes (extract, classify, summarize). +* **Balanced** GÇö `model_tier: "core"` with `thinking_depth: "fast"`. Solid precision, moderate speed and cost. Good default for most batch flows. +* **High-stakes** GÇö `model_tier: "pro"` or `"max"`. Use when the cost of a wrong edit (lawyer review hours, regulatory exposure, reputational damage) far exceeds the per-document token cost GÇö i.e. always for legal, regulatory, medical, or financial documents. + +## Choosing for precision + +If the AI's output isn't what you wanted, the right tier change is usually obvious once you name the symptom: + +| Symptom | What to try | +| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **AI edited sections you didn't ask about** | Switch to `model_tier: "pro"` or `"max"`. Also verify your prompt is single-section explicit (e.g. "edit Section 3" rather than "fix the document"). Vague prompts invite wide edits. | +| **Edits miss nuance in legal / regulatory / medical language** | `model_tier: "max"` with `thinking_depth: "deep"`. Max is the most capable model and Deep gives it room to reason carefully about wording. | +| **Too slow** | `model_tier: "turbo"` (loses some precision but is the fastest). Or stay on `core` and pass `thinking_depth: "fast"` (smaller cost, smaller precision drop). | +| **Want a balance GÇö don't know which to pick** | Stay on the default: `core` + `balanced`. The model picks reasoning effort dynamically per request. Most edits won't need deep reasoning; complex ones will. | +| **Want maximum precision, cost is no object** | `model_tier: "max"` + `thinking_depth: "deep"`. Most expensive, most accurate. | + +## What `balanced` actually does + +`thinking_depth: "balanced"` is **dynamic** GÇö the model decides how much reasoning to apply based on the prompt's complexity. Most everyday edits trigger minimal reasoning (cheap and fast); complex multi-step edits trigger more (slower, more expensive). This is the recommended default for general editing, and it's what `core + balanced` gives you out of the box. + +`fast` (a tight per-tier reasoning ceiling) is faster but can over-narrow on the AI's part GÇö sometimes it picks a wider scope than the prompt warranted because it didn't have budget to reason carefully about scope. Use `fast` when you've verified your prompts are unambiguous and you want the speed. + +`deep` (a large per-tier reasoning ceiling) gives the model room to reason carefully. Use it for high-stakes edits where one bad output is more expensive than the extra tokens. + +## When to use Deep GÇö and what it costs + +Deep reasoning roughly **6+ù the output tokens** versus Balanced for a typical 1,000-token edit. On `core` that translates to roughly $0.04 vs $0.007 per edit; on `max` it's higher GÇö roughly $0.05 vs $0.009. The wall-clock latency increase is typically 1GÇô3 seconds. + +That cost is well worth it when: + +* The document is high-stakes (a contract clause, a regulatory disclosure, a medical instruction) +* Output errors are expensive to fix downstream (executive review, lawyer hours) +* You're running few-but-important edits, not high-volume batch work + +It is NOT worth it when: + +* You're doing fast iterative editing (a writer's draft, a Slack-formatting cleanup) +* You're processing high volumes (use `turbo` instead) +* The user is sitting in front of the screen waiting (latency dominates UX) + + + **For legal, regulatory, medical, financial, or compliance documents GÇö default to Pro or Max.** The cost of one wrong edit on a contract clause or a HIPAA-relevant medical instruction massively exceeds the cost of running a more capable tier. Don't optimise for token cost on documents where the human cost of a mistake is in lawyer hours, regulatory exposure, or patient harm. + + +## Default model preference + +You can set a default model tier in your account preferences (via the web app). Per-request `model_tier` overrides your default. + + +# Multi-Document Sessions +Source: https://docs.superdocs.app/guides/multi-document + +Work with several open documents in one session GÇö target any of them by id, let the AI move content between them, or have it open a brand-new document. + +# Multi-Document Sessions + +A SuperDocs session can hold **several open documents at once** GÇö like tabs in an editor. The AI sees all of them, can read or search any of them, and edits whichever one your request targets. One document is always the **focused** document: the default target for any chat turn that doesn't say otherwise. + +Everything here works identically over the REST API and the MCP tools. + +## The mental model + +* **Session** GÇö one conversation thread with the AI. +* **Open documents** GÇö the set of documents loaded in that session, each with a stable `document_id` and a title. +* **Focused document** GÇö the one a turn targets when you don't specify a `document_id`. Uploading with default settings replaces it; single-document integrations never need to think about any of this (one document = it's always focused). + +## Opening more documents + +`POST /v1/documents/upload` accepts an `open_mode` form field: + +| `open_mode` | Behavior | +| ------------------- | ---------------------------------------------------------------------------------- | +| `replace` (default) | The uploaded file replaces the focused document GÇö the pre-multi-document behavior. | +| `new_focused` | Opens the file as a **new** document and focuses it. Existing documents stay open. | +| `background` | Opens the file as a new document but keeps the current focus. | + +(`new_tab` is also accepted as an alias of `new_focused`.) + + + **`open_mode` is on the multipart upload only.** `POST /v1/documents/upload` (multipart) is where you choose `new_focused` / `background`. The agent-facing upload tools GÇö `upload_document_base64` and the pre-signed `process_uploaded_document` GÇö don't take `open_mode`; they load the file as the focused document (replacing the current one). For an agent to open an additional document as a new tab, open an already-saved document with `open_documents` / `init_session`, or just ask the AI in chat to put the content in a new document. So this one upload detail is **not** identical across REST-multipart and the MCP upload tools. + + +The upload response includes the session's full document roster (document ids, a ready `title` for each, section counts, and which one is focused), so you can render tabs immediately GÇö use the `title` field for the tab label. The roster is token-light by default: it returns metadata only and the HTML body is omitted (`null`). Pass `include_html=true` when you actually need the body. + +The AI can also **create** a new document itself: ask in chat GÇö "put the summary in a new document called Q3 Summary" GÇö and the turn opens a fresh document with the generated content (this bills one operation like other content creation). + +## Listing, focusing, closing + +```bash theme={null} +# Roster: every open document + which one is focused. +# Metadata only by default (ids, title, section counts, focused flag) GÇö token-light; +# add ?include_html=true to also get each document's HTML body. +curl https://api.superdocs.app/v1/sessions/my-session/documents \ + -H "Authorization: Bearer sk_YOUR_API_KEY" + +# Switch focus +curl -X POST https://api.superdocs.app/v1/sessions/my-session/documents/{document_id}/focus \ + -H "Authorization: Bearer sk_YOUR_API_KEY" + +# Close one document (the session stays alive); optionally pick the next focus +curl -X DELETE "https://api.superdocs.app/v1/sessions/my-session/documents/{document_id}?next_focus={other_id}" \ + -H "Authorization: Bearer sk_YOUR_API_KEY" +``` + +The same three operations are MCP tools: `list_session_documents`, `focus_session_document`, `close_session_document`. + +## Targeting a document in chat + +Three ways, in order of precedence: + +1. **Explicit `document_id`** on the chat request (REST `/v1/chat`, `/v1/chat/async`, or the MCP `chat`/`chat_async` tools). That document also becomes the focus for the turn. +2. **Name it in the message** GÇö "fix the totals in the invoice" routes to the open document titled like an invoice. The AI resolves references the way a colleague would, including follow-ups like "now add those notes to the meeting doc". +3. **Neither** GÇö the turn targets the focused document. + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/chat \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "message": "Standardize the date format in this document", + "session_id": "my-session", + "document_id": "doc_2f6cGǪ" + }' +``` + +The AI can also work **across** documents in one turn: read one document and apply what it found to another ("use the rates from the rate card to fill the invoice"), search all open documents at once, or update several documents in a single request. + +## What changes on the wire + +If you only ever open one document per session, nothing changes. With multiple documents: + +* `proposed_change_batch` (review mode): every change in `changes[]` carries its `document_id`, so you can group the approval list per document. See [SSE Streaming](/guides/streaming#proposed_change_batch). +* `documents_changed`: emitted when an auto-apply turn changed 2+ documents, or when a document was **created** (in either approval mode) GÇö so background tabs can badge and new tabs appear. See [SSE Streaming](/guides/streaming#documents_changed). +* The `final` result includes `focused_document_id`. +* Document payloads (upload responses, the roster, session history) may include a nullable `page_setup` object GÇö the page size, orientation, and margins detected from `.docx`/PDF uploads GÇö useful if your UI renders true-size pages. + +## Reverting without a whole-document swap + +`POST /v1/sessions/{id}/revert` rewinds the conversation and the document to the state just before a chosen user message (see [Revert a session](/concepts/sessions#revert-a-session-to-a-previous-message) for the base flow). For a multi-document integration, three extra fields let you apply a revert as precisely as you apply a normal AI turn GÇö and undo it if needed. + +**Preview before you commit (`dry_run`).** Send `"dry_run": true` to compute what the revert *would* change without committing or archiving anything. The response comes back with `dry_run: true` and a populated `revert_changes` you can show the user ("this will roll back 3 sections in the invoice") before they confirm. Nothing is touched until you call it again without `dry_run`. + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/sessions/my-session/revert \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"turn_index": 4, "dry_run": true}' +``` + +**Apply per document, not by reloading (`revert_changes`).** The revert response carries `revert_changes` GÇö a map of document id GåÆ the changes for that document. Apply each document's changes the same way you apply a normal AI result: per section, onto the live editor (see [Applying AI updates safely](/guides/editor-integration#applying-ai-updates-safely-without-losing-user-edits)). This matters in a multi-document session: a single revert can touch several open documents, and a user may be mid-edit in one of them. Applying `revert_changes` section-by-section preserves their in-progress typing and lets the user keep their version and accept the other as a suggestion on a clashed section, exactly like a live edit. Whole-document `setContent` per tab would throw that away. `document_state` + `editor_action` are still returned for the focused document as a fallback for simple single-document UIs. + +**Undo a revert (`redo`).** Revert is non-destructive. The response includes `redo_checkpoint_id` GÇö the pre-revert head. Pass it (with the same `turn_index`) to `POST /v1/sessions/{id}/redo` to fork forward to the pre-revert state and un-archive exactly the turns the revert hid. `redo` returns the same shape as `revert` (including its own `revert_changes` to re-apply per section). It's meant for use **immediately** after a revert GÇö once a new chat message is sent the branch diverges, so stop offering redo at that point. `redo_checkpoint_id` is `null` on a dry-run and on a first-message reset (there's nothing to fork forward to). + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/sessions/my-session/redo \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"turn_index": 4, "redo_checkpoint_id": "ckpt_GǪ"}' +``` + +While a chat job on the session is in progress or awaiting approval, both `revert` and `redo` return `409` GÇö settle or cancel the job first. + +## When an edit fails or goes wrong + +Multi-document turns are designed so you can always tell what state each document is in GÇö nothing is left ambiguous after a failure: + +* **Failed turns say so, and bill 0.** If a requested edit could not be applied, the response text reports the real outcome instead of claiming success, and no operation is charged. A turn that partially applied reports per-section results: `document_changes.changes_summary` summarizes what actually changed (e.g. "2 sections edited, 1 added, rest untouched"), and `change_history` / compact-mode `chunk_diffs` carry the per-section before/after for audit. +* **Bad output is reversible per document.** If the AI writes a malformed structure (say, into a nested table), you don't have to reload anything: preview the rollback with `POST /v1/sessions/{id}/revert` + `"dry_run": true`, inspect the per-document `revert_changes` map, then commit the revert GÇö and `redo` remains available immediately afterward if the user changes their mind. See "Reverting without a whole-document swap" above. +* **Versions are the ground truth.** Every stored document carries a monotonically increasing `version`; each committed change (AI turn, autosave, revert, another session's edit) bumps it. A response's `document_changes` reflects the committed state, and genuine same-section concurrent conflicts are surfaced explicitly GÇö a `concurrent_merges` block plus a notice appended to the reply GÇö never resolved silently. Cross-session changes announce themselves as [document events](/guides/concurrent-editing#polling-for-other-sessions-edits) you can poll. + +## Notes + +* Document order in the roster is stable insertion order GÇö safe to use as tab order. +* Session history restore (`get_session_history`) returns the conversation plus the **focused** document's state; call `list_session_documents` to rebuild the full tab set when restoring a multi-document session. +* Closing a document doesn't delete anything from your storage; it just removes it from the session. + + +# Server Integration +Source: https://docs.superdocs.app/guides/server-integration + +Use SuperDocs from a backend service with no UI GÇö batch document processing, queue workers, scheduled jobs, document workflows. No frontend, no SSE consumer, no human approval needed. + +# Server Integration + +**Quick path:** new to the API? Go from an API key to your first successful call in the [Quickstart](/introduction/quickstart) (\~5 minutes), then come back here for the full backend integration. + +If your product is a backend service that processes documents GÇö a queue worker editing contracts overnight, a scheduled job that summarizes incoming reports, a document workflow that auto-improves files before they hit your storage GÇö SuperDocs slots in as a synchronous API call, no UI required. + +This guide covers the pattern + working snippets for the five most common backend stacks (Node, Python, Go, Ruby, .NET), plus when to choose sync vs async vs polling. + + + **For server-side workflows on high-stakes documents** (contracts, regulatory filings, medical records, financial statements) GÇö default to `model_tier: "pro"` in your request body. There's no human in the loop to catch a wrong edit, so spend the extra tokens on a more capable tier. See [Model Selection](/guides/model-selection) for the full matrix. + + +## Why this shape is different from a UI integration + +In a typical SuperDocs UI integration, a user types in a chat panel, the chat opens an SSE stream, and the user reviews proposed edits in real time before approving. The whole architecture is shaped around interactive review. + +In a server-side integration, none of that applies. There's no user typing, no chat panel, no SSE consumer to build, and (typically) no human approval. Your service has documents in a queue or storage, calls SuperDocs synchronously to get them edited, persists the results, and moves on. + +The integration is simpler GÇö usually one synchronous HTTP call per document, plus a wrapper around it for retries, error handling, and persistence. + +## The pattern, in 4 steps + +1. **Get a document HTML** from wherever your service stores documents (S3, database, file, message queue payload). +2. **Call `POST /v1/chat`** synchronously with `approval_mode: "approve_all"`. SuperDocs applies all proposed changes automatically and returns the final updated HTML. +3. **Persist the updated HTML** back to wherever the document lives. +4. **Handle errors and retries** per your service's existing conventions. + +That's the whole loop. The snippets below are the same loop in five different language idioms. + +## When to use sync vs async vs polling + +Three SuperDocs endpoints can drive a server-side integration. Pick based on document size and your service's needs: + +| Endpoint | When to use | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `POST /v1/chat` (sync) | Default. Documents under \~50 pages, single-call edits, batch processing. Simplest pattern: one HTTP call in, one HTTP response out. | +| `POST /v1/chat/async` + `GET /v1/jobs/{job_id}` (polling) | Long-running edits where you want to fire-and-forget and check back. Documents over \~50 pages, complex multi-section rewrites, or workflows where you don't want to block on a single HTTP call. | +| `POST /v1/chat/async` + SSE stream | Rare for server-side. Use only if you specifically want to react to intermediate progress events or the turn's `proposed_change_batch` event. Most server integrations don't need this. | + +For 90% of server-side integrations, the sync `/v1/chat` endpoint is the right answer. + +## Node.js (TypeScript) + +```typescript theme={null} +import 'dotenv/config'; + +const SUPERDOCS_KEY = process.env.SUPERDOCS_API_KEY!; + +interface SuperDocsResponse { + response: string; + document_changes: { updated_html: string; version_id?: string }; + usage: { monthly_used: number; monthly_limit: number; monthly_remaining: number }; +} + +export async function editDocument( + message: string, + sessionId: string, + documentHtml: string, + approvalMode: "approve_all" | "ask_every_time" = "approve_all" +): Promise { + const res = await fetch("https://api.superdocs.app/v1/chat", { + method: "POST", + headers: { + "Authorization": `Bearer ${SUPERDOCS_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + message, + session_id: sessionId, + document_html: documentHtml, + approval_mode: approvalMode, + }), + // Sync edits on long documents can take 30-60 seconds; raise default timeout. + signal: AbortSignal.timeout(300_000), + }); + + if (!res.ok) { + throw new Error(`SuperDocs ${res.status}: ${await res.text()}`); + } + return res.json() as Promise; +} + +// Example queue worker GÇö pulls a document, edits it, persists the result +export async function processQueueItem(jobId: string, documentHtml: string, instruction: string) { + try { + const result = await editDocument(instruction, `job-${jobId}`, documentHtml); + await persistToStorage(jobId, result.document_changes.updated_html); + return { ok: true, jobId, opsRemaining: result.usage.monthly_remaining }; + } catch (err) { + console.error(`Failed to process ${jobId}:`, err); + throw err; // Let your queue's retry logic decide + } +} + +async function persistToStorage(jobId: string, html: string) { + // Replace with your storage idiom: S3 upload, database UPDATE, file write, etc. +} +``` + +## Python (FastAPI / Django / standalone) + +```python theme={null} +import os, httpx +from typing import Literal, TypedDict + +SUPERDOCS_KEY = os.environ["SUPERDOCS_API_KEY"] + +class SuperDocsResponse(TypedDict): + response: str + document_changes: dict + usage: dict + +def edit_document( + message: str, + session_id: str, + document_html: str, + approval_mode: Literal["approve_all", "ask_every_time"] = "approve_all", +) -> SuperDocsResponse: + """Sync edit. For long documents (50+ pages), prefer the async + polling pattern.""" + with httpx.Client(headers={"Authorization": f"Bearer {SUPERDOCS_KEY}"}, timeout=300) as client: + response = client.post( + "https://api.superdocs.app/v1/chat", + json={ + "message": message, + "session_id": session_id, + "document_html": document_html, + "approval_mode": approval_mode, + }, + ) + response.raise_for_status() + return response.json() + +# Example queue worker (Celery, RQ, plain script GÇö pattern is the same) +def process_queue_item(job_id: str, document_html: str, instruction: str) -> dict: + try: + result = edit_document(instruction, f"job-{job_id}", document_html) + persist_to_storage(job_id, result["document_changes"]["updated_html"]) + return {"ok": True, "job_id": job_id, "ops_remaining": result["usage"]["monthly_remaining"]} + except httpx.HTTPStatusError as e: + # SuperDocs API returned non-2xx GÇö let your queue's retry logic decide. + # Most errors (rate limit, transient network) are worth retrying. + # 401 (bad key), 4xx with explicit messages, are NOT worth retrying. + if e.response.status_code in (429, 502, 503, 504): + raise # Let queue retry + return {"ok": False, "job_id": job_id, "error": str(e), "retryable": False} + +def persist_to_storage(job_id: str, html: str): + # Replace with your storage idiom. + pass + +# Async / polling variant for long documents (50+ pages) +def edit_document_async(message: str, session_id: str, document_html: str) -> str: + """Returns updated HTML after polling job completion.""" + import time + with httpx.Client(headers={"Authorization": f"Bearer {SUPERDOCS_KEY}"}, timeout=60) as client: + # Kick off the job + job_response = client.post( + "https://api.superdocs.app/v1/chat/async", + json={"message": message, "session_id": session_id, + "document_html": document_html, "approval_mode": "approve_all"}, + ) + job_response.raise_for_status() + job_id = job_response.json()["job_id"] + + # Poll until complete (typically 10-60 seconds for non-trivial edits) + while True: + time.sleep(2) + status = client.get(f"https://api.superdocs.app/v1/jobs/{job_id}").json() + if status["status"] == "completed": + return status["result"]["document_changes"]["updated_html"] + if status["status"] == "failed": + raise RuntimeError(f"Job failed: {status.get('error')}") +``` + +## Go (net/http) + +```go theme={null} +package superdocs + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "time" +) + +var key = os.Getenv("SUPERDOCS_API_KEY") + +type EditRequest struct { + Message string `json:"message"` + SessionID string `json:"session_id"` + DocumentHTML string `json:"document_html"` + ApprovalMode string `json:"approval_mode"` +} + +type EditResponse struct { + Response string `json:"response"` + DocumentChanges struct { + UpdatedHTML string `json:"updated_html"` + VersionID string `json:"version_id,omitempty"` + } `json:"document_changes"` + Usage struct { + MonthlyUsed int `json:"monthly_used"` + MonthlyLimit int `json:"monthly_limit"` + MonthlyRemaining int `json:"monthly_remaining"` + } `json:"usage"` +} + +func EditDocument(ctx context.Context, req EditRequest) (*EditResponse, error) { + if req.ApprovalMode == "" { + req.ApprovalMode = "approve_all" + } + body, _ := json.Marshal(req) + + httpReq, _ := http.NewRequestWithContext(ctx, "POST", + "https://api.superdocs.app/v1/chat", bytes.NewReader(body)) + httpReq.Header.Set("Authorization", "Bearer "+key) + httpReq.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 300 * time.Second} + res, err := client.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("superdocs request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + b, _ := io.ReadAll(res.Body) + return nil, fmt.Errorf("superdocs %d: %s", res.StatusCode, string(b)) + } + + var out EditResponse + if err := json.NewDecoder(res.Body).Decode(&out); err != nil { + return nil, fmt.Errorf("decode: %w", err) + } + return &out, nil +} + +// Example queue worker pattern +func ProcessQueueItem(ctx context.Context, jobID, documentHTML, instruction string) error { + result, err := EditDocument(ctx, EditRequest{ + Message: instruction, + SessionID: "job-" + jobID, + DocumentHTML: documentHTML, + }) + if err != nil { + return err + } + return persistToStorage(jobID, result.DocumentChanges.UpdatedHTML) +} + +func persistToStorage(jobID, html string) error { + // Your storage idiom here + return nil +} +``` + +## Ruby (Rails service object) + +```ruby theme={null} +require "net/http" +require "uri" +require "json" + +class SuperDocsService + KEY = ENV.fetch("SUPERDOCS_API_KEY") + BASE = "https://api.superdocs.app" + + def self.edit_document(message:, session_id:, document_html:, approval_mode: "approve_all") + uri = URI("#{BASE}/v1/chat") + req = Net::HTTP::Post.new(uri, { + "Authorization" => "Bearer #{KEY}", + "Content-Type" => "application/json", + }) + req.body = JSON.generate({ + message: message, + session_id: session_id, + document_html: document_html, + approval_mode: approval_mode, + }) + + res = Net::HTTP.start(uri.host, uri.port, + use_ssl: true, read_timeout: 300) { |h| h.request(req) } + raise "SuperDocs #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess) + JSON.parse(res.body) + end +end + +# Example background job (Sidekiq / Active Job) +class EditDocumentJob < ApplicationJob + queue_as :default + retry_on StandardError, wait: :exponentially_longer, attempts: 3 + + def perform(document_id, instruction) + document = Document.find(document_id) + result = SuperDocsService.edit_document( + message: instruction, + session_id: "doc-#{document_id}", + document_html: document.html, + ) + document.update!(html: result["document_changes"]["updated_html"]) + end +end +``` + +## .NET (C#, minimal API + service) + +```csharp theme={null} +using System.Net.Http.Json; +using System.Text.Json.Serialization; + +public class SuperDocsService +{ + private readonly HttpClient _http; + private readonly string _key = Environment.GetEnvironmentVariable("SUPERDOCS_API_KEY")!; + + public SuperDocsService(HttpClient http) + { + _http = http; + _http.Timeout = TimeSpan.FromSeconds(300); + _http.DefaultRequestHeaders.Add("Authorization", $"Bearer {_key}"); + } + + public async Task EditDocumentAsync( + string message, string sessionId, string documentHtml, + string approvalMode = "approve_all", CancellationToken ct = default) + { + var req = new { + message, + session_id = sessionId, + document_html = documentHtml, + approval_mode = approvalMode, + }; + var res = await _http.PostAsJsonAsync("https://api.superdocs.app/v1/chat", req, ct); + res.EnsureSuccessStatusCode(); + return (await res.Content.ReadFromJsonAsync(cancellationToken: ct))!; + } +} + +public record EditResponse( + [property: JsonPropertyName("response")] string Response, + [property: JsonPropertyName("document_changes")] DocumentChanges DocumentChanges, + [property: JsonPropertyName("usage")] Usage Usage +); + +public record DocumentChanges( + [property: JsonPropertyName("updated_html")] string UpdatedHtml, + [property: JsonPropertyName("version_id")] string? VersionId +); + +public record Usage( + [property: JsonPropertyName("monthly_used")] int MonthlyUsed, + [property: JsonPropertyName("monthly_limit")] int MonthlyLimit, + [property: JsonPropertyName("monthly_remaining")] int MonthlyRemaining +); + +// Example background worker +public class DocumentProcessingWorker(SuperDocsService superdocs, ILogger log) +{ + public async Task ProcessAsync(string jobId, string documentHtml, string instruction) + { + try { + var result = await superdocs.EditDocumentAsync(instruction, $"job-{jobId}", documentHtml); + await PersistToStorageAsync(jobId, result.DocumentChanges.UpdatedHtml); + log.LogInformation("Processed {JobId}, ops remaining: {Remaining}", + jobId, result.Usage.MonthlyRemaining); + } catch (HttpRequestException ex) { + log.LogError(ex, "SuperDocs failed for {JobId}", jobId); + throw; + } + } + + private Task PersistToStorageAsync(string jobId, string html) => Task.CompletedTask; +} +``` + +## Persisting `updated_html` to storage + +Where the result goes depends on your service's existing storage. Common patterns: + +* **Database row.** `UPDATE documents SET html = $1, updated_at = now() WHERE id = $2;`. Make sure your column type accommodates the document size GÇö `text` in Postgres, `LONGTEXT` in MySQL. +* **S3 / object storage.** `s3.put_object(Bucket="...", Key="documents/{id}.html", Body=updated_html)`. Versioning at the bucket level gives you free history. +* **Local file.** `with open(f"output/{job_id}.html", "w") as f: f.write(updated_html)`. Fine for batch jobs that produce files. +* **Message queue.** Publish the updated HTML to a downstream queue for the next step in your pipeline. + +The `data-chunk-id` attributes that SuperDocs adds to block-level elements **must round-trip** if you plan to send the document back to SuperDocs again later (e.g., for further edits in the same session). Most string-based storage preserves them automatically; if you parse the HTML through a sanitizer or DOM library, ensure unknown attributes survive. + + + If a document your server edits can **also** be open in another session at the same time (a user editing in your app's UI while a background job rewrites the same file, or two workers on one document), don't let the two writers clobber each other. Poll for the other session's edits and apply them per section, and persist your own typing through the autosave endpoint so everyone converges. See [Concurrent & Cross-Session Editing](/guides/concurrent-editing). + + +## Error handling and retries + +SuperDocs returns standard HTTP status codes. Worth handling: + +* **401 / 403** GÇö bad or missing API key. Not retryable; surface immediately. +* **429** GÇö rate limit. Wait and retry with exponential backoff. Check the `Retry-After` header. +* **5xx** GÇö transient server error. Retry with backoff. +* **4xx (other)** GÇö bad request, malformed HTML, unsupported parameters. Read the response body for the actual error and fix the request shape; not retryable. + +For batch / queue workers, lean on your queue's existing retry mechanism (Sidekiq's `retry_on`, Celery's `autoretry_for`, AWS SQS dead-letter queues, etc.) rather than building retry logic in the SuperDocs wrapper itself. + +## Auto-approve vs human-out-of-band + +If your server-side workflow needs a human's sign-off before applying SuperDocs' edits GÇö even though there's no interactive UI GÇö you have two patterns: + +* **Send a notification (Slack, email, SMS) with the proposed change.** The notification includes a button or reply mechanism. When the human responds, your service POSTs to `/v1/chat/{session_id}/approve` to apply the change. Use `approval_mode: "ask_every_time"` and stream the proposed changes via SSE to your notification dispatcher. +* **Queue proposed changes for batch human review.** The human reviews a list of pending changes in your existing admin UI (or a CSV export, or a Slack digest). Same `ask_every_time` + approve POST pattern, just with a delayed human in the loop. + +## Stuck? + +If your server stack isn't covered here or your workflow needs a pattern not described above, email [hello@superdocs.app](mailto:hello@superdocs.app) or book a 15-minute integration call at [cal.com/superdocs](https://cal.com/superdocs). We'll talk through the pattern and add a snippet for the next person. + +## Related guides + +* [Async Jobs](/guides/async-jobs) GÇö when you want polling instead of sync `/v1/chat`. +* [SSE Streaming](/guides/streaming) GÇö when you want streaming progress events from a server-side consumer. +* [Agent Tool Integration](/guides/agent-tool-integration) GÇö when your service is wrapping an AI agent that needs SuperDocs as a tool. +* [Human-in-the-Loop](/guides/human-in-the-loop) GÇö when you need human approval, even out-of-band. +* [Concurrent & Cross-Session Editing](/guides/concurrent-editing) GÇö when the same document can be edited from more than one place at once. +* [Integration Starter Prompt](/guides/integration-starter-prompt) GÇö paste this into your coding agent to wire all the above up automatically. + + +# SSE Streaming +Source: https://docs.superdocs.app/guides/streaming + +Stream real-time progress updates from the AI using Server-Sent Events (SSE). + +# SSE Streaming + +**Quick path:** new to the API? Go from an API key to your first successful call in the [Quickstart](/introduction/quickstart) (\~5 minutes), then come back here to add streaming. + +For long-running AI operations, use Server-Sent Events (SSE) to receive real-time progress updates instead of waiting for the full response. + + + SSE is one option, not the only option. For non-browser consumers (server-side workers, batch jobs, AI agent tools), the synchronous [`/v1/chat`](/examples/curl) endpoint or [polling on `/v1/jobs/{id}`](/guides/async-jobs) are simpler and equally valid. See [Server Integration](/guides/server-integration) for the patterns. + + + + **Revert + active SSE.** If your UI lets users [revert a session](/concepts/sessions#revert-a-session-to-a-previous-message) while an SSE stream is still open, close the `EventSource` first (or wait for the `final` event) before sending the revert call. Revert returns `409` while a chat job is in flight on the session, and any updates that arrive after revert from the original branch are stale. + + +## How it works + +1. Start an async chat request to get a `job_id` +2. Open an SSE connection to stream progress +3. Receive events as the AI processes your request + +## Setup + +```bash theme={null} +# 1. Start an async request +curl -X POST https://api.superdocs.app/v1/chat/async \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "message": "Rewrite all sections to be more concise", + "session_id": "my-session", + "document_html": "..." + }' + +# Response: { "job_id": "550e8400-e29b-41d4-a716-446655440000", "session_id": "my-session", ... } + +# 2. Stream progress (auth via query parameter) +curl -N "https://api.superdocs.app/v1/chat/my-session/stream?job_id=550e8400-e29b-41d4-a716-446655440000&api_key=sk_YOUR_API_KEY" +``` + + + SSE uses EventSource, which doesn't support custom headers. Pass your API key as the `api_key` query parameter. + + +## Event types + +The stream emits nine event types: [`intermediate`](#intermediate), [`proposed_change_batch`](#proposed_change_batch), [`document_sync`](#document_sync), [`continue_prompt`](#continue_prompt), [`documents_changed`](#documents_changed), [`model_fallback`](#model_fallback), [`final`](#final), [`usage`](#usage), and [`error`](#error). + +### `intermediate` + +Progress updates during AI processing. + +``` +event: intermediate +data: {"type": "intermediate", "content": "Analyzing document structure...", "sequence": 1, "timestamp": "2026-03-07T10:00:01Z"} +``` + +#### Rendering intermediate events in your UI + +Show every `intermediate` event to users in real time. Operations on large documents (or with `model_tier: "max"` + `thinking_depth: "deep"`) can take 30 seconds to several minutes GÇö without visible progress, your UI looks frozen and indistinguishable from a crash. + +**Pattern 1 GÇö Append-and-update an in-flight chat bubble (recommended for chat-style UIs):** + +```javascript theme={null} theme={null} +let inFlightBubble = null; + +eventSource.addEventListener("intermediate", (event) => { + const data = JSON.parse(event.data); + + if (!inFlightBubble) { + // First intermediate of this turn GÇö create a new bubble in the in-flight state. + inFlightBubble = { + id: "progress-" + Date.now(), + role: "assistant", + content: data.content, + status: "in_flight", + }; + chatLog.push(inFlightBubble); + renderChatLog(); + } else { + // Subsequent intermediates GÇö replace the bubble's content with the latest update. + inFlightBubble.content = data.content; + renderChatLog(); + } +}); + +eventSource.addEventListener("final", (event) => { + const data = JSON.parse(event.data); + if (inFlightBubble) { + // Promote the in-flight bubble to the final response. + inFlightBubble.content = data.content; + inFlightBubble.status = "complete"; + inFlightBubble = null; + } + renderChatLog(); +}); +``` + +**Pattern 2 GÇö Streaming text indicator (compact toolbar / status bar):** + +```javascript theme={null} theme={null} +const statusEl = document.querySelector("#ai-status"); +let lastTs = Date.now(); + +eventSource.addEventListener("intermediate", (event) => { + const data = JSON.parse(event.data); + statusEl.textContent = data.content; + statusEl.classList.add("active"); + lastTs = Date.now(); +}); + +// Surface a "still processing" hint if no event arrives for 30s. +setInterval(() => { + if (statusEl.classList.contains("active") && Date.now() - lastTs > 30000) { + statusEl.textContent += " (still working GÇö large operations can take a few minutes)"; + } +}, 5000); + +eventSource.addEventListener("final", () => { + statusEl.classList.remove("active"); + statusEl.textContent = ""; +}); +``` + +**Cadence expectations.** Intermediate events typically arrive every 1GÇô5 seconds during active processing. A gap of 30+ seconds usually means the AI is in a deep reasoning phase GÇö surface a "still processing" hint rather than a "failed" indicator. Use the `timestamp` field to detect stalls programmatically. + +### `proposed_change_batch` + +In HITL mode (`approval_mode: "ask_every_time"`), the AI proposes document changes for review. **`proposed_change_batch` is the only event that carries proposed changes** GÇö every HITL turn arrives through it, whether the AI proposes one change or hundreds. The whole turn comes as **one** SSE event carrying a `changes` array, so the wire load stays proportional to turns, not changes, and HITL UIs can render the entire approval card at once. + + + Older clients may still register a separate `proposed_change` listener. That's harmless GÇö keep it for safety if you have it GÇö but it never fires: single-change turns are delivered through `proposed_change_batch` too (with `type: "single_approval"`, a `changes` array of length 1). Don't build logic that waits for or branches on a standalone `proposed_change` event; it will never arrive. + + +``` +event: proposed_change_batch +data: {"type": "proposed_change_batch", "content": "{\"batch_id\": \"ch_1\", \"batch_total\": 32, \"changes\": [...]}", "sequence": 2} +``` + +The `content` field is a JSON-stringified string that must be parsed once more: + +```javascript theme={null} theme={null} +eventSource.addEventListener("proposed_change_batch", (event) => { + const data = JSON.parse(event.data); + const batch = JSON.parse(data.content); + // batch.batch_id, batch.batch_total, batch.changes[*] +}); +``` + + + `proposed_change_batch.content` is delivered as a **JSON-stringified string**, not a parsed object. The event data itself is JSON, and the `content` field inside it is a second layer of JSON that must be parsed again (see the double-parse above). This is inconsistent with `final.result`, which is already an object after a single parse. Missing the second parse is the single most common reason integrators see empty diff cards GÇö the UI renders but every field reads as `undefined`. + + +In sessions with multiple open documents, every change in `changes[]` also carries the `document_id` it belongs to (see the field table below), so your UI can group the review list per document. + +Edits to page headers, footers, footnote/endnote bodies, and comments arrive through this same event, each as its own ordinary entry in `changes[]` (the part's block is its `chunk_id`). There is no separate event or approval track for them; approve or deny them like any other change. + +The parsed payload: + +```json theme={null} +{ + "type": "batch_approval", + "batch_id": "ch_1", + "batch_total": 32, + "changes": [ + { + "change_id": "ch_1", + "operation": "edit", + "chunk_id": "550e8400-e29b-41d4-a716-446655440000", + "old_html": "

Original...

", + "new_html": "

Updated...

", + "ai_explanation": "Tightened phrasing", + "insert_after_chunk_id": null + }, + { + "change_id": "ch_2", + "operation": "edit", + "chunk_id": "660f9511-...", + "old_html": "...", + "new_html": "...", + "ai_explanation": "...", + "insert_after_chunk_id": null + } + ] +} +``` + +**Top-level payload:** + +| Field | Type | Description | +| ------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------- | +| `type` | string | `"batch_approval"` for multi-change turns, `"single_approval"` for one-change turns. Both shapes arrive through this same event. | +| `batch_id` | string | Shared identifier across the batch GÇö equals the first change's `change_id`. | +| `batch_total` | number | Number of entries in `changes`. | +| `changes` | array | The proposed changes GÇö one entry per change (see fields below). | + +**Each entry in `changes[]`:** + +| Field | Type | Description | +| ----------------------- | -------------- | --------------------------------------------------------------------------------------------------- | +| `change_id` | string | Unique ID for this change. Use when calling `/approve`. | +| `operation` | string | `"edit"`, `"create"`, or `"delete"`. | +| `chunk_id` | string \| null | The document section being modified or deleted. Null for creates. | +| `old_html` | string \| null | Current HTML content. Present for updates and deletes. Null for creates. | +| `new_html` | string \| null | Proposed new HTML content. Present for updates and creates. Null for deletes. | +| `ai_explanation` | string | Why the AI proposed this change. Show this to the user. | +| `insert_after_chunk_id` | string \| null | For `create`: where to insert the new section in the document. | +| `document_id` | string | In multi-document sessions, the open document this change belongs to GÇö group the review list by it. | + +See the [Human-in-the-Loop guide](/guides/human-in-the-loop) for the complete approval workflow. + +### `continue_prompt` + +For a very large edit, the AI completes as much as it can in one turn, keeps that work, and asks whether to continue with the rest. This can happen in either approval mode. Resume (or stop) by calling [`POST /v1/chat/{session_id}/continue`](/guides/async-jobs#continuing-a-large-edit). + +``` +event: continue_prompt +data: {"type": "continue_prompt", "content": "{\"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}"} +``` + +The `content` field is a JSON string GÇö parse it for: + +* `message` GÇö a ready-to-display prompt, already in the user's language. +* `done` / `total` / `remaining` GÇö progress counts you can show alongside it. + +```javascript theme={null} theme={null} +eventSource.addEventListener("continue_prompt", (event) => { + const payload = JSON.parse(JSON.parse(event.data).content); + // Show payload.message and a Continue / Stop choice, then POST the decision: + // POST /v1/chat/{session_id}/continue { "job_id": "...", "continue": true } +}); +``` + +Send `continue: true` to keep going or `false` to stop and keep what's done. The job resumes and may emit another `continue_prompt` for the next segment GÇö handle it in a loop, the same way you handle repeated HITL approval rounds. + +### `document_sync` + +Emitted before the AI begins processing, after the backend has prepared your document for editing. The event carries the prepared HTML containing the section identifiers the AI will reference when proposing changes. + +``` +event: document_sync +data: {"type": "document_sync", "content": "

Section 1

...", "focused_document_id": "d1"} +``` + +**When it fires**: Only when you provide `document_html` in the request. The event arrives once at the start of the stream, before any `intermediate` or `proposed_change_batch` events. + +**What to do with it**: Apply the HTML to your editor immediately so the editor's section IDs match the IDs the AI will reference in subsequent `proposed_change_batch` events. This is essential for HITL diff highlights to render correctly on freshly pasted or uploaded documents GÇö without it, the editor and the AI may disagree on which section a change targets. + + + **Multi-document sessions.** When several documents are open, `document_sync` carries `focused_document_id` GÇö the document this prepared HTML belongs to. Apply it only to the matching (focused) tab, not whichever document happens to be on screen. Single-document sessions can ignore the field. + + +If you do not render diffs in an editor (e.g., you only display the final response), you can ignore this event. + +### `documents_changed` + +Emitted when a turn touched **more than one open document** on auto-apply, or **created a new document** (creation applies immediately in either approval mode). Tells your UI which documents changed so non-focused tabs can badge and newly created tabs appear GÇö for review-mode *edits*, `proposed_change_batch` already carries per-change `document_id`, so this event isn't needed there. + +``` +event: documents_changed +data: {"type": "documents_changed", "content": "{\"documents\": [{\"document_id\": \"d1\", \"title\": \"Invoice\", \"change_count\": 3, \"changed_chunk_ids\": [\"c1\", \"c2\", \"c7\"], \"created\": false}], \"focused_document_id\": \"d1\"}"} +``` + +**Payload shape** (JSON-encoded in `content`): + +* `documents[]` GÇö one entry per changed document: `document_id`, `title`, `change_count`, `changed_chunk_ids` (sections to flash on tab switch), and `created` (`true` when the AI opened this as a brand-new document this turn GÇö add a tab for it; its `change_count` is `0`). +* `focused_document_id` GÇö the document currently in focus after the turn. + +**When it fires**: on auto-apply (`approval_mode: "approve_all"`) turns that changed 2+ documents, and on any turn GÇö **either approval mode** GÇö where the AI created a new document (creation applies immediately even in review mode). Standard single-document auto-apply edit turns don't emit it (the focused doc flashes inline); it may still fire for multi-document assistant/sub-agent turns. Ignore it if your integration only ever works with one document per session. + +### `final` + +Job completed successfully. Contains the full result. + +``` +event: final +data: {"content": "I've made all sections more concise.", "result": {"response": "...", "document_changes": {...}, "usage": {...}}} +``` + +In multi-document sessions, `result` also includes `focused_document_id` so you know which open document the turn finished on. + +### `usage` + +Emitted after `final` with usage consumption data. + +``` +event: usage +data: {"monthly_used": 43, "monthly_limit": 500, "monthly_remaining": 457, "was_billable": true, "subscription_tier": "free"} +``` + +### `error` + +Job failed, was cancelled, or an auth error occurred. + +``` +event: error +data: {"error": "Request timed out"} +``` + +### `model_fallback` + +Emitted only when the AI tier you requested is temporarily unresponsive upstream and SuperDocs **automatically completed your request on the `pro` tier instead**. Your request still succeeds GÇö this event just tells you (and lets you tell your users) that a different tier served it. + +``` +event: model_fallback +data: {"type": "model_fallback", "content": "{\"from_tier\": \"core\", \"to_tier\": \"pro\"}"} +``` + +**What to do with it**: optional. Show a small notice ("the default model is busy GÇö this response used the Pro tier") or just log it. Billing is unchanged GÇö the operation counts exactly as it normally would. During healthy operation this event never fires. + +## Reconnect & resume + +Every event carries a monotonically increasing `sequence` number. If your EventSource connection drops mid-job, reconnect with the `last_sequence` query parameter set to the highest sequence you already processed GÇö the stream replays **only newer events**, never the full history: + +```bash theme={null} +curl -N "https://api.superdocs.app/v1/chat/my-session/stream?job_id=550e8400-e29b-41d4-a716-446655440000&last_sequence=17&api_key=sk_YOUR_API_KEY" +``` + +Omit `last_sequence` (or pass `0`) to receive the full event history for the job GÇö useful when a fresh client attaches to an already-running job. This also makes reconnecting after `approve_change` safe: already-rendered `proposed_change_batch` / `document_sync` events are not re-delivered. + +## JavaScript example + +```javascript theme={null} +const eventSource = new EventSource( + `https://api.superdocs.app/v1/chat/my-session/stream?job_id=${jobId}&api_key=${apiKey}` +); + +eventSource.addEventListener("document_sync", (event) => { + const data = JSON.parse(event.data); + // Apply data.content to your editor so its section IDs match + // the IDs the AI will reference in proposed_change_batch events. + editor.commands.setContent(data.content); +}); + +eventSource.addEventListener("intermediate", (event) => { + const data = JSON.parse(event.data); + console.log("Progress:", data.content); +}); + +eventSource.addEventListener("final", (event) => { + const data = JSON.parse(event.data); + console.log("Done:", data.result.response); + // Update your editor with data.result.document_changes.updated_html + eventSource.close(); +}); + +eventSource.addEventListener("usage", (event) => { + const data = JSON.parse(event.data); + console.log(`Used ${data.monthly_used}/${data.monthly_limit} operations`); +}); + +eventSource.addEventListener("error", (event) => { + if (event.data) { + const data = JSON.parse(event.data); + console.error("Error:", data.error); + } + eventSource.close(); +}); +``` + +## Python example + +```python theme={null} +import json +import requests + +url = f"https://api.superdocs.app/v1/chat/my-session/stream?job_id={job_id}&api_key={api_key}" + +with requests.get(url, stream=True) as response: + for line in response.iter_lines(): + if not line: + continue + decoded = line.decode("utf-8") + if decoded.startswith("data: "): + data = json.loads(decoded[6:]) + if "content" in data: + print(f"Progress: {data['content']}") + elif "error" in data: + print(f"Error: {data['error']}") + break +``` + + +# For AI Agents +Source: https://docs.superdocs.app/introduction/agent-signup + +Sign up and get a working SuperDocs API key in one call, with no human in the loop. + +# For AI Agents + +SuperDocs is built to be used by AI agents, not just people. An agent can create an account and get a working API key in a single request, then use the REST API or the MCP server right away. No web sign-up form, no waiting on a human. + + + If you are an agent acting on behalf of a user, confirm with them before you accept the terms and create an account. + + +## 0. Check for existing credentials first + +If `~/.superdocs/agent_credentials.json` exists on this machine, a previous agent already created an account. Call `GET https://api.superdocs.app/v1/agents/whoami` with that `api_key` and **reuse the account** GÇö its documents and remaining quota carry over. Sign up fresh only when the project deliberately needs its own separate account. + +## 1. Sign up + +```bash curl theme={null} +curl -X POST https://api.superdocs.app/v1/agents/signup \ + -H "Content-Type: application/json" \ + -d '{"terms_accepted": true, "agent_name": "your-agent-name"}' +``` + +The response returns everything you need, including the API key (shown only once): + +```json theme={null} +{ + "account_id": "GǪ", + "slug": "your-agent-name-a1b2c3", + "email": "your-agent-name-a1b2c3@agents.superdocs.app", + "api_key": "sk_GǪ", + "quota": { "tier": "free", "monthly_limit": 500, "used": 0, "remaining": 500 }, + "endpoints": { + "mcp": "https://api.superdocs.app/mcp/", + "chat": "https://api.superdocs.app/v1/chat", + "whoami": "https://api.superdocs.app/v1/agents/whoami", + "openapi": "https://api.superdocs.app/openapi.json" + }, + "mcp_setup": { + "claude_code": "claude mcp add -s user --transport http superdocs https://api.superdocs.app/mcp/ --header \"Authorization: Bearer sk_GǪ\"", + "generic_mcp": "Point any MCP client at https://api.superdocs.app/mcp/ (Streamable HTTP) with header Authorization: Bearer ", + "rest_fallback": "Prefer REST? POST https://api.superdocs.app/v1/chat with the Bearer header", + "docs": "https://docs.superdocs.app/account/mcp-setup" + }, + "handoff": { "how": "When you near the 500-op cap, POST /v1/agents/handoff with your key and {email}; you get back a short takeover code to give your human, who then adopts the account and can pay to continue" } +} +``` + +The `mcp_setup` block gives you copy-paste commands to connect right away. + + + Save `api_key` now. It is shown only once. Store the whole response at `~/.superdocs/agent_credentials.json` and call `GET /v1/agents/whoami` before signing up again so you reuse the same account instead of creating a new one. + + +Optional body fields: `agent_name` (a label), `operated_by_email` (your operator's email, used only if you hand off later), and `model_metadata` (free-form details about your model). + +## 2. Use your key + +Send the key as a Bearer token on every request, on the REST API or the MCP server. + +```bash REST theme={null} +curl -X POST https://api.superdocs.app/v1/chat \ + -H "Authorization: Bearer sk_YOUR_KEY" \ + -H "Content-Type: application/json" \ + -d '{"message": "Draft a one-page NDA", "session_id": "s1"}' +``` + +For MCP, point your client at `https://api.superdocs.app/mcp/` (streamable HTTP) with the same Bearer key. In Claude Code, one command connects it: + +```bash Claude Code theme={null} +claude mcp add -s user --transport http superdocs https://api.superdocs.app/mcp/ \ + --header "Authorization: Bearer sk_YOUR_KEY" +``` + +Other clients (Cursor, Claude Desktop, Windsurf, VS Code, Cline, Continue) take the same URL and header. See [MCP Setup](/account/mcp-setup). Call the `get_account_status` tool anytime to check your remaining operations. + +### Long generations + +For document-scale generations (a whole handbook or long report in one turn), use `POST /v1/chat/async` GÇö it returns a `job_id` immediately and reports progress via `GET /v1/jobs/{job_id}`. Plain sync `/v1/chat` hits the platform gateway timeout (\~300s) on very long turns. Editing patterns, placement, verification, and billing are covered in the [Agent editing playbook](/guides/agent-editing-playbook). + +## 3. Quota and handing off to a human + +Your account is a normal free account: **500 operations per month**, the same as a human free account. When you near the cap and want to keep going, hand the account to your human so they can take it over and pay: + +```bash curl theme={null} +curl -X POST https://api.superdocs.app/v1/agents/handoff \ + -H "Authorization: Bearer sk_YOUR_KEY" \ + -H "Content-Type: application/json" \ + -d '{"email": "your-human@example.com", "working_context": "the ~/Documents/acme project on your Mac"}' +``` + +Pass `working_context` (a short note on where you are running) so the email is recognizable and does not look like spam. The response includes a short **`takeover_code`** (like `ABCD-1234`). + +**Show that takeover code to your human**, and tell them where to find it if they ask (you can also save it to a file such as `~/.superdocs/takeover-code.txt`). We email a one-time link to the address you gave. Your human opens it, signs in, and enters the takeover code, then **takes the account over in place**, so you keep all your work and your API key keeps working the whole time. They can then upgrade or pay from the app. + +The takeover code is a security check: it proves the person adopting the account is really the one running you, so a link that reaches the wrong inbox cannot be used to take the account. Never email the code or post it anywhere public. (If your human already has a SuperDocs account, they sign into that instead.) + + + **Adopt-link errors:** a wrong takeover code returns `403` with the number of attempts remaining in `detail`. After too many wrong codes the link is permanently disabled and returns `410 Gone`; that old link can never work again, so send a new `POST /v1/agents/handoff` to get a fresh one. An expired or unknown link returns `404` (links expire after about 24 hours). + + +You can also just wait: your quota resets at the start of each month. + + + Fully autonomous with no human who can upgrade for you? `POST https://api.superdocs.app/v1/agents/request-upgrade` with your key (optionally a short `note`). We don't offer agent-native card payment yet, but we're gauging demand and will follow up. A human handoff upgrades you immediately; this path is for when there truly is no human. + + +## Rules and limits + +* Use **one account per agent or user**. Do not create multiple accounts to get around the free limit. +* Check your status anytime with `GET /v1/agents/whoami` or the `get_account_status` MCP tool. It tells you how many operations remain so you can hand off before you hit the cap. + + +# Overview +Source: https://docs.superdocs.app/introduction/overview + +SuperDocs is an AI document editing platform. Edit, create, search, and manage documents through a REST API or MCP. + +# SuperDocs + +SuperDocs is an AI-powered document editing platform. Send a document and a natural language instruction, and the AI edits the document directly GÇö adding sections, rewriting content, restructuring, or analyzing what's there. + + + **From zero to your first AI-powered document edit in 5 minutes. A complete integration in under 15 minutes.** + + + + **Not to be confused with "SuperDoc."** SuperDocs (this platform, at [superdocs.app](https://superdocs.app)) is a hosted AI document-editing service you reach over a REST API and an MCP server. It is a separate, unaffiliated product from the similarly-named open-source **SuperDoc** in-browser DOCX editor at [superdoc.dev](https://superdoc.dev). If you want an embeddable editor component, that's the other project; if you want AI that edits documents for you over an API, you're in the right place. + + +## Three ways to use SuperDocs + + + + Open the editor at use.superdocs.app. Write, paste, or upload a document. Chat with the AI to edit it. + + + + 50+ REST endpoints plus 38 MCP tools for chat, documents, sessions, attachments, jobs, and templates. Any language, any platform. + + + + Connect from Claude Desktop, Cursor, VS Code, or Claude Code. The AI tools appear natively in your editor. + + + + + **AI agent with no human in the loop?** Sign up in one call: `POST https://api.superdocs.app/v1/agents/signup` with `{"terms_accepted": true, "agent_name": "your-name"}` GÇö the response carries a working API key + copy-paste MCP setup. Check `~/.superdocs/agent_credentials.json` first and reuse an existing account via `GET /v1/agents/whoami`. Full guide: [Agent signup](/introduction/agent-signup) -+ [Agent editing playbook](/guides/agent-editing-playbook). + + +## Which path fits you + +| If you are... | Choose | Start here | +| -------------------------------------------------------------------------------------------------------------------------- | --------------------- | ---------------------------------------------- | +| A person who wants to edit documents in a browser, no code | **Web app** | [use.superdocs.app](https://use.superdocs.app) | +| A developer adding document AI to a product, backend, or script | **REST API** | [Quickstart](/introduction/quickstart) | +| Working inside an AI tool (Claude Code, Claude Desktop, Cursor, VS Code) and want SuperDocs tools available natively there | **MCP** | [MCP Setup](/mcp/setup) | +| An autonomous AI agent that needs its own account, with no human in the loop | **Agent self-signup** | [Agent signup](/introduction/agent-signup) | + +All four paths reach the same platform and the same documents; they differ only in how you connect. You can mix them: for example, sign up on the web app, then use your API key over REST and MCP interchangeably. + +## What you can do + +* **Edit documents** GÇö "Add a confidentiality clause to section 3" +* **Create content** GÇö "Draft a project proposal for a mobile app" +* **Search and analyze** GÇö "Find all sections that mention liability" +* **Work with multiple documents at once** GÇö open several documents in one session like tabs ([multi-document sessions](/guides/multi-document)) +* **Keep durable Files** GÇö documents persist across sessions, so you can list, reopen, rename, and archive them ([Files](/concepts/files)) +* **Reach across sessions** GÇö opt-in [cross-session memory and search](/concepts/cross-session-context) so the AI can carry your preferences and reuse your own prior work +* **Edit concurrently** GÇö the same document open in two places stays in sync without lost edits ([concurrent editing](/guides/concurrent-editing)) +* **Work with attachments** GÇö Upload PDFs or documents for AI context +* **Review changes** GÇö Approve or deny AI-proposed edits before they're applied +* **Revert any message** GÇö Rewind both chat and document to the state before any user message +* **Choose your AI model** GÇö Four model tiers from fast to most capable + +## Next steps + + + Get your API key and make your first request in under 5 minutes. + + +## Building with AI? + +If you're using an AI coding agent to build your integration, give it the complete reference file GÇö all documentation in a single file optimized for AI consumption. + + + docs.superdocs.app/llms-full.txt GÇö feed this to your AI agent for full context. + + + +# Quickstart +Source: https://docs.superdocs.app/introduction/quickstart + +Get your API key and make your first SuperDocs API call in under 5 minutes. + +# Quickstart + +Go from zero to your first AI-powered document edit in 5 minutes. + +## 1. Create an account + +Sign up at [use.superdocs.app](https://use.superdocs.app) with Google or email/password. Free plan includes 500 operations per month GÇö an **operation** is one AI request that modifies, analyzes, or searches a document (uploads/parsing and exports are free; very large edits count one operation per 25 sections changed). Full definition and examples: [Plans & Usage](/account/plans-and-usage#what-counts-as-an-operation). + +## 2. Create an API key + +1. Click the **gear icon** in the app to open Settings +2. Go to the **API Keys** tab +3. Click **Create API Key** and give it a name +4. **Copy the key immediately** GÇö it's only shown once + +Your key looks like: `sk_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4` + + + Building an autonomous agent? You can skip the UI entirely. One call to `POST /v1/agents/signup` returns a working key with no human in the loop. See [For AI Agents](/introduction/agent-signup). + + +## 3. Make your first request + + + ```bash curl theme={null} + curl -X POST https://api.superdocs.app/v1/chat \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "message": "Add a paragraph about data privacy at the end", + "session_id": "my-first-session", + "document_html": "

Company Policy

Welcome to our company.

" + }' + ``` + + ```python Python theme={null} + import requests + + response = requests.post( + "https://api.superdocs.app/v1/chat", + headers={"Authorization": "Bearer sk_YOUR_API_KEY"}, + json={ + "message": "Add a paragraph about data privacy at the end", + "session_id": "my-first-session", + "document_html": "

Company Policy

Welcome to our company.

" + } + ) + + data = response.json() + print(data["response"]) + ``` + + ```javascript JavaScript theme={null} + const response = await fetch("https://api.superdocs.app/v1/chat", { + method: "POST", + headers: { + "Authorization": "Bearer sk_YOUR_API_KEY", + "Content-Type": "application/json" + }, + body: JSON.stringify({ + message: "Add a paragraph about data privacy at the end", + session_id: "my-first-session", + document_html: "

Company Policy

Welcome to our company.

" + }) + }); + + const data = await response.json(); + console.log(data.response); + ``` +
+ + + **You choose the `session_id`.** It's any string you pick, not a value the server assigns or returns. There's no "create session" call: the first request that uses a new string starts that session, and reusing the same string continues it. Pick something stable per document or per user (e.g. `user_123_draft-contract`). See [Sessions](/concepts/sessions). + + +## 4. Read the response + +```json theme={null} +{ + "response": "I've added a data privacy paragraph at the end of the document.", + "session_id": "my-first-session", + "document_changes": { + "updated_html": "

Company Policy

...
", + "changes_summary": "Document updated by AI" + }, + "usage": { + "monthly_used": 1, + "monthly_limit": 500, + "monthly_remaining": 499, + "was_billable": true, + "subscription_tier": "free" + } +} +``` + +Key fields: + +* `response` GÇö The AI's reply explaining what it did +* `document_changes.updated_html` GÇö The full updated document HTML. Render this in your editor. +* `usage` GÇö How many operations you've used this month + +## 5. Continue the conversation + +Reuse the same `session_id` to keep editing. The server keeps your document between turns, so on a follow-up you send just the `message`. No need to re-send `document_html`: + +```bash theme={null} +curl -X POST https://api.superdocs.app/v1/chat \ + -H "Authorization: Bearer sk_YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "message": "Make the data privacy section more formal", + "session_id": "my-first-session" + }' +``` + +The AI picks up the document it already holds for this session and returns the updated HTML again. + + + **When to re-send `document_html`:** only when *you* changed the HTML outside of chat. For example, the user edited the document in your own editor and you want the AI to see those edits. Then send the full current HTML (exactly as your editor has it, `data-chunk-id` attributes intact) and it replaces the server's copy. If chat is the only thing touching the document, omit `document_html` on every turn after the first. See [Documents](/concepts/documents#sending-document-html). + + +## Next steps + + + + How session persistence works + + + + How to handle document HTML + + + + Upload files for AI context + + + + Connect from Claude Desktop or Cursor + + + + Give your AI agent the complete reference file + + + + +# Available Tools +Source: https://docs.superdocs.app/mcp/available-tools + +All 38 MCP tools available when connecting to the SuperDocs MCP server. + +# Available MCP Tools + +When you connect to the SuperDocs MCP server, these 38 tools become available to your AI tool. Each tool is designed to surface SuperDocs' real capabilities GÇö structural editing of styled documents, multi-document sessions, durable Files that persist across sessions, optional cross-session memory and search, fidelity-preserving export, multimodal vision, human-in-the-loop approval, and large-file upload/download without bloating agent context. + +## Chat + +| Tool | Description | Key parameters | +| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `chat` | Edit, draft, or restructure a document using natural language. Preserves tables, styling, and formatting. Returns AI response and structural changes. Sessions can hold **multiple open documents** GÇö pass `document_id` to target a specific one (omit it to work on the focused document), or just name the document in your message; the AI can also open a brand-new document on request ("put this summary in a new document"), or duplicate an existing document and change only its values while keeping the original layout and formatting exactly ("recreate this invoice for a new client with these amounts"). **For large documents (>20 pages), set `response_mode='compact'`** to skip the full HTML in the response and get only per-section diffs (`chunk_diffs`) instead GÇö saves thousands of tokens per turn. To read sections, just ask in natural language ("show me the force majeure section") and the AI returns the content in the reply. **Optional cross-session context** (all OFF by default, owner-scoped): set `cross_session_memory=true` to let the AI carry durable preferences from this API key's prior sessions, and `cross_session_search=true` to let it search your own earlier documents and chats. | `message`, `session_id`, `document_html`, `document_id`, `model_tier`, `thinking_depth`, `approval_mode`, `response_mode`, `image_attachments`, `cross_session_memory`, `cross_session_search`, `cross_session_memory_key`, `cross_session_scope` | +| `chat_async` | Start a long-running or HITL-approved AI edit; returns a job\_id to poll. Use for multi-step edits or `approval_mode='ask_every_time'`. Accepts the same `document_id` targeting and the same optional `cross_session_*` context fields as `chat`. **For large documents, set `response_mode='compact'`** so polled job results skip the full HTML and surface only per-section diffs. A long edit may pause partway through and ask to continue GÇö resume it with `continue_chat` (see below), not `approve_change`. | `message`, `session_id`, `document_html`, `document_id`, `model_tier`, `thinking_depth`, `approval_mode`, `response_mode`, `cross_session_memory`, `cross_session_search`, `cross_session_memory_key`, `cross_session_scope` | +| `approve_change` | Approve or deny AI-proposed document changes one-by-one or in batch (HITL workflow). Supports per-change feedback for the AI to revise on. | `session_id`, `job_id`, `change_id`, `approved`, `feedback`, `changes` | +| `continue_chat` | Resume a large edit that paused partway through and asked whether to keep going. When a job is awaiting approval and the pause is a continue prompt (a `continue_prompt` streaming event was sent), call this with `continue=true` to finish the remaining work, or `continue=false` to stop and keep what's already applied. Use this GÇö **not** `approve_change` GÇö for this kind of pause. | `job_id`, `continue` | + + + **Cross-session context is opt-in and owner-scoped.** `cross_session_memory` and `cross_session_search` default to **off** GÇö turn them on per request. Everything stays scoped to the API key that owns it; the AI never reaches into another account's sessions. For B2B2C integrators serving many end-customers under one key, pass `cross_session_memory_key` (a stable per-end-customer key, so each customer gets their own durable memory note) and/or `cross_session_scope` (a list of session ids to narrow cross-session search to that customer's work). Wipe the memory note any time with `clear_cross_session_memory` GÇö note its parameter is named `memory_key` (you pass the same value you sent as `cross_session_memory_key` on chat). + + +## Documents + +| Tool | Description | Key parameters | +| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | +| `upload_document_base64` | Upload `.docx` / PDF / HTML / MD / RTF as the active editable document with chunk-ID structural editing. Tables, borders, shading, and styling preserved on edit and export. Also works for AI-generated outlines. **For files >100KB, prefer the `request_upload_url` flow below** GÇö base64 through the agent's context is slow and token-expensive at scale. | `filename`, `file_base64`, `session_id` | +| `request_upload_url` | Get a pre-signed URL to upload large files (`.docx` / PDF / HTML / MD / RTF) without bloating agent context. Returns a 5-minute PUT URL plus a ready-to-run curl example GÇö your agent shells the file directly to cloud storage, so bytes never pass through the context window. After upload, call `process_uploaded_document` with the returned `upload_id`. Max file size: 100 MB. Supported formats: `.pdf`, `.docx`, `.txt`, `.rtf`, `.md`, `.html`, `.htm`. | `filename`, `content_type`, `size_bytes`, `purpose` | +| `process_uploaded_document` | Parse a file uploaded via `request_upload_url` into structured HTML with chunk IDs for targeted AI editing. Same parsing pipeline as `upload_document_base64` GÇö tables, borders, shading, alternating row colors, fonts, and inline styling preserved on edit and export. Pass `parse_mode='document'` to load as the active editable document, or `parse_mode='attachment'` to load as a read-only AI-searchable reference. **Returns a compact result by default** (`chunks_count`, `version_id`, `page_setup`; `html` is `null`) so the parsed body never floods agent context GÇö pass `return_html=true` only if you actually need the HTML back. **Loading a file is not billed** GÇö you're only charged when the AI later edits it. | `upload_id`, `filename`, `session_id`, `parse_mode`, `return_html` | +| `export_document` | Export the current document with full fidelity. Five output formats GÇö `docx` (default, Microsoft Word Open XML), `pdf` (paginated, print-ready), `html` (standalone with inlined CSS), `markdown` (`.md` with ATX headings), `txt` (plain text). Pass an `options` object to customise paper size, orientation, margins, filename, image embedding, and optional PDF watermarks. Non-fatal issues surface via the `X-Export-Warnings` response header. For documents over 20 MB, use the `request_upload_url` GåÆ PUT GåÆ `export_document` with `upload_id` flow. | `session_id`, `html`, `upload_id`, `format`, `options`, `filename` | +| `request_download_url` | Re-export the session's current document and get a pre-signed URL to download it without proxying through the agent. Returns a 15-minute GET URL plus a curl example GÇö your agent fetches the file directly from cloud storage. Same five formats as `export_document` (`format='pdf'` produces a native PDF); pass `options` to customise the export. Use this when the user wants to retrieve a finished document at scale. | `session_id` (required), `format`, `filename`, `options` | +| `upload_image_base64` | Upload an image (PNG/JPG/GIF/WebP, base64) to use inside documents GÇö it returns a hosted URL you can reference in chat ("insert my logo at the top"). The agent-friendly way to get local image bytes into a document. | `filename`, `image_base64`, `session_id` | + +## Sessions + +| Tool | Description | Key parameters | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------ | +| `list_sessions` | List your active document editing sessions to resume or audit prior work. | `limit` | +| `get_session_history` | Restore the full conversation and document state for a previous session, including chunk IDs, attachments, and editor actions. Returns the focused document's HTML by default; pass `include_document_html=false` to skip the body and get conversation + metadata only (lighter on tokens when resuming a large document). | `session_id`, `include_document_html` | +| `get_session_jobs` | List all async chat jobs for a specific session, most recent first. Pass `compact=true` to omit each job's heavy result payload (status/progress only) GÇö useful when you just want to see which jobs are paused. | `session_id`, `limit`, `compact` | +| `revert_session_to_message` | Rewind a chat session to before a specific user message GÇö restores both the document and the conversation in one call. Returns the reverted message text so you can pre-fill a compose box for editing. The original branch is preserved server-side but hidden from active reads. | `session_id`, `turn_index` | +| `redo_revert` | Undo a revert. Restores the session forward to the pre-revert state and brings back the rolled-back conversation and document. Pass the same `turn_index` you reverted plus the `redo_checkpoint_id` returned by that revert. Meant for use immediately after a revert, before sending a new message. The mirror of `revert_session_to_message`. | `session_id`, `turn_index`, `redo_checkpoint_id` | + +## Session documents (multi-document) + +A session can hold several open documents at once GÇö like tabs in an editor. One document is always the **focused** document (the default target for chat turns that don't specify `document_id`). + +| Tool | Description | Key parameters | +| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | +| `list_session_documents` | List every open document in a session GÇö each one's id, title, section count, page geometry, and which one is focused. Token-light by default: the HTML body is omitted unless you pass `include_html=true`. Document ids from this roster are what you pass as `document_id` on `chat`/`chat_async` GÇö and every such parameter ALSO accepts the roster's `durable_document_id` (the permanent Files id), so you can drive a session entirely with durable ids. | `session_id`, `include_html` | +| `focus_session_document` | Switch the session's focused document. Subsequent chat turns without an explicit `document_id` target the newly focused document. | `session_id`, `document_id` | +| `close_session_document` | Close an open document in the session. Optionally pass `next_focus` to choose which remaining document takes focus. The session itself stays alive. | `session_id`, `document_id`, `next_focus` | + + + **Roster reads are token-light by default.** `list_session_documents` (and the Files reads + `open_documents`/`init_session` below) return each document's `title` plus metadata, but **not** the HTML body, by default GÇö perfect for picking a target or labelling tabs without spending tokens. Pass `include_html=true` only when you actually need the document body. + + +## Files (durable documents) + +Documents in SuperDocs are **durable** GÇö they persist across sessions and survive restarts, like files on disk. Open a saved document into any new session to keep working on it, rename it, or archive it when you're done. Archiving is a soft delete: archived documents are recoverable for about 30 days, then purged. + +| Tool | Description | Key parameters | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | +| `list_documents` | List your durable documents (across all sessions) to find one to reopen, rename, or archive. Returns each document's `title` and metadata by default; pass `include_html=true` for bodies. | `limit`, `include_html` | +| `get_document_detail` | Get one durable document by id GÇö its `title`, metadata, and a free `structure` outline (headings with levels/positions, section count, block count, media counts, plus a `parts` list naming any page headers, footers, footnotes, endnotes, and comments with their ids) by default, or the full HTML body with `include_html=true`. `structure` is the cheap way to verify an edit landed GÇö never export just to check. | `document_id`, `include_html` | +| `rename_document` | Rename a durable document. The new title is what shows up in `list_documents` and as the tab label when it's open. | `document_id`, `title` | +| `archive_document` | **Destructive (soft delete).** Archive a durable document so it no longer appears in the active list. It's recoverable for about 30 days, then permanently purged. Use only when the user explicitly wants the document removed. If the document is open in another session the call fails with 409 `document_in_use` (nothing archived) GÇö re-call with `force=true` or close it there first. | `document_id`, `force` | +| `unarchive_document` | Restore (un-archive) a previously archived durable document so it returns to the active list. The clean inverse of `archive_document` GÇö it takes the same durable `document_id`, so you can round-trip archive Gåö restore by id (no session needed). Idempotent GÇö restoring an already-active document is a no-op. | `document_id` | +| `open_documents` | Open one or more saved durable documents into a session GÇö like opening files into tabs. Optionally choose which one takes focus. Returns the resulting roster (`title` + metadata; `include_html=true` for bodies). | `session_id`, `document_ids`, `focus_document_id`, `include_html` | +| `init_session` | Create a new session and open saved durable documents into it in a single call GÇö a shortcut for "start a fresh session with these files already loaded." | `session_id`, `document_ids`, `focus_document_id`, `include_html` | + +## Cross-session memory + +SuperDocs can optionally carry a small durable memory note across an API key's sessions (see the opt-in `cross_session_memory` field on `chat`/`chat_async`). This tool manages that note. + +| Tool | Description | Key parameters | +| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | +| `clear_cross_session_memory` | **Destructive.** Wipe the cross-session memory note for this API key (or for a specific end-customer when you pass `memory_key`). The note is gone permanently GÇö the AI starts fresh on the next session that opts into cross-session memory. **The parameter is named `memory_key`** (pass the same value you send as `cross_session_memory_key` on chat) GÇö omitting it clears the account-level note, so set it to target a specific end-customer. | `memory_key` | + +## Attachments + +| Tool | Description | Key parameters | +| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | +| `upload_attachment_base64` | Upload a reference file (PDF/DOCX/image) for the AI to query while editing. Processed asynchronously; queryable via semantic search and multimodal vision. | `filename`, `file_base64`, `session_id` | +| `delete_attachment` | Remove an attachment from a session or cancel its in-progress processing. | `attachment_id`, `session_id` | +| `get_attachment_status` | Check processing status of all attachments in a session. Poll after upload to know when ready. | `session_id` | + +## Jobs + +| Tool | Description | Key parameters | +| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | +| `list_jobs` | List your async chat jobs (in-progress, awaiting approval, completed, failed). Pass `compact=true` to omit each job's heavy result payload and get a lightweight status/progress list. | `status`, `limit`, `compact` | +| `get_job` | Get the status, partial results, and any pending changes for an async chat job. Poll after `chat_async`. | `job_id` | +| `cancel_job` | Cancel a pending or in-progress async chat job. Already-applied changes are preserved. | `job_id` | + +## Account + +| Tool | What it does | Key parameters | +| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | +| `get_account_status` | Your account at a glance: tier, operations used/remaining this month, reset date, and whether the account has been adopted by a human. The first thing to call when resuming work with saved credentials, and the cheapest way to budget a long job. Non-billable. | GÇö | +| `request_limit_increase` | Request higher document/chat scale limits for your account in one call GÇö use after a `413` with `DOCUMENT_TOO_COMPLEX` (single document past the standard per-document page limit) or `SESSION_TOO_FULL` (documents open in one chat past the per-chat limit). These are stability limits, not hard caps: the team is notified immediately with your account identity and the numbers you send, and limits are typically expanded within a day. Optional fields: `kind`, `attempted_pages`, `attempted_sections`, `filename`, `note`. Non-billable. | GÇö | + + + **Running as an autonomous agent?** You can create your own account and `sk_` API key in one request (no web form, no human), then check quota anytime with `get_account_status`. When you near your monthly cap, hand the account to a human: `POST /v1/agents/handoff` with their email returns a one-time **takeover code**. They open an emailed link, enter the code, and adopt the account in place, so your work and this API key keep working. No human? `POST /v1/agents/request-upgrade`. Full flow (signup GåÆ handoff GåÆ adopt) is on the [For AI Agents](/introduction/agent-signup) page. (Signup, handoff, adopt, and request-upgrade are REST endpoints, not MCP tools.) + + +## Templates + +| Tool | Description | Key parameters | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | +| `upload_template_base64` | Save a document template (NDA, contract, SOP, letterhead) for reuse across sessions. Referenceable by the AI when drafting new documents. | `filename`, `file_base64` | +| `list_user_templates` | List all saved document templates available to the user or organization. | GÇö | +| `delete_user_template` | Delete a saved document template by ID (soft-delete). | `template_id` | + +## Health + +| Tool | Description | Key parameters | +| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | +| `health` | Verify the SuperDocs MCP server is reachable and serving traffic. Runs over your authenticated `/mcp/` connection like every MCP tool, so it also confirms your API key is wired up correctly (a `401` means a key problem, not downtime). The REST equivalent, `GET https://api.superdocs.app/health`, requires no authentication. | GÇö | + + + **Two upload paths GÇö pick by file size.** For **files larger than 100KB**, use the pre-signed URL flow (`request_upload_url` GåÆ upload via curl GåÆ `process_uploaded_document`). Bytes stream directly to cloud storage and never pass through your agent's context window, saving thousands of tokens per upload. For **files smaller than 100KB** where token cost is trivial, `upload_document_base64` still works inline GÇö encode to base64 and pass the original `filename` (used for file type detection). Supported file types: `.pdf`, `.docx`, `.txt`, `.rtf`, `.md`, `.html`, `.htm`. Maximum file size: 100 MB. + + + + **Compact response mode for editing large documents.** When working with documents larger than \~20 pages, set `response_mode='compact'` on `chat` and `chat_async`. Instead of returning the full updated HTML on every turn (\~130K tokens for a 100-page styled doc), the response includes only `chunk_diffs` GÇö the per-section before/after for chunks that were actually changed. To read sections, just ask in natural language ("show me the force majeure clause") and the AI returns the content in the reply text. A 5-turn editing session on a 100-page doc drops from \~700K tokens to \~3K tokens this way. + + +For full request/response schemas, open the **API Reference** tab in the top navigation (auto-generated from the OpenAPI spec). For copy-paste request examples, see [cURL examples](/examples/curl) and the [Quickstart](/introduction/quickstart). + + +# Claude Code +Source: https://docs.superdocs.app/mcp/claude-code + +Connect SuperDocs to Claude Code (CLI) with a single command. + +# Claude Code + +The fastest way to connect SuperDocs to Claude Code is the official plugin GÇö it bundles the MCP server (38 tools), the 4 workflow prompts, and the auto-loading skill in one install. Manual MCP config also works as a fallback. + +## Install via plugin (recommended) + +In a Claude Code session, add the SuperDocs marketplace, then install the plugin from it: + +```bash theme={null} +claude plugin marketplace add superdocsapp/superdocs-plugin +claude plugin install superdocs@superdocs-plugin +``` + +The install argument is `plugin-name@marketplace-name` (`superdocs@superdocs-plugin`), not the GitHub repo path. Passing the repo path to `install` directly (`claude plugin install superdocsapp/superdocs-plugin`) fails with "doesn't exist in marketplace". That repo path is only valid for `marketplace add`. + +You'll be prompted for your SuperDocs API key (`sk_GǪ`). It's stored securely in your OS keychain GÇö never in plaintext config. Get a key from [use.superdocs.app](https://use.superdocs.app) GåÆ **Settings GåÆ API Keys GåÆ Create**. Free plan includes 500 AI operations per month. + +After install: + +* All 38 MCP tools are available +* 4 workflow prompts appear in your `/` slash menu (`/superdocs:draft_from_outline`, `/superdocs:edit_styled_docx`, `/superdocs:convert_format`, `/superdocs:review_contract_for_redflags`) +* The SuperDocs skill auto-loads when you ask document-editing questions + +For full plugin reference (auto-update behavior, troubleshooting, advanced config), see [/mcp/plugin](/mcp/plugin). + +## Manual setup (advanced) + +If you prefer to configure the MCP server directly without the plugin (e.g., to share a `.mcp.json` across a team via project scope), run: + +```bash theme={null} +claude mcp add -s user --transport http superdocs https://api.superdocs.app/mcp/ \ + --header "Authorization: Bearer sk_YOUR_API_KEY" +``` + +Both `/mcp` and `/mcp/` are accepted; the trailing-slash form is the canonical spelling. + +That installs only the MCP server (38 tools). To get the 4 prompts and the skill, you'd need to set those up separately GÇö the plugin install above does all three in one shot. + +## Verify the connection + +```bash theme={null} +claude mcp list +``` + +You should see: + +``` +superdocs: https://api.superdocs.app/mcp/ (HTTP) - G£ô +``` + +If you see `G£ù Failed to connect`, restart Claude Code (`Ctrl+C` then `claude` in a new shell). MCP servers load at session startup, so a fresh session is required after `claude mcp add`. + +## Test it + +```bash theme={null} +claude +# Then type: "Use the SuperDocs health tool to check the API" +``` + +## Example usage + +Once connected, you can ask Claude Code to: + +* "Create a technical specification document using SuperDocs" +* "Edit my SuperDocs session and update the introduction" +* "Upload this file as an attachment to my SuperDocs session" + +## Running autonomously? + +If you're an AI agent driving Claude Code with no human watching, you can also self-serve an account and API key in one request, then hand it to a human near your quota cap using a one-time takeover code. See [For AI Agents](/introduction/agent-signup). + +## Remove the server + +```bash theme={null} +claude mcp remove superdocs +``` + + +# Claude Desktop +Source: https://docs.superdocs.app/mcp/claude-desktop + +Step-by-step guide to connect SuperDocs to Claude Desktop via MCP. + +# Claude Desktop + +Connect SuperDocs to Claude Desktop so you can edit documents through conversation. + +## 1. Find your config file + +| OS | Path | +| ------- | ----------------------------------------------------------------- | +| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` | +| Windows | `%APPDATA%\Claude\claude_desktop_config.json` | + +Create the file if it doesn't exist. + +## 2. Add the SuperDocs config + +```json claude_desktop_config.json theme={null} +{ + "mcpServers": { + "superdocs": { + "command": "npx", + "args": [ + "-y", + "mcp-remote@latest", + "https://api.superdocs.app/mcp/", + "--header", + "Authorization:${SUPERDOCS_AUTH}" + ], + "env": { + "SUPERDOCS_AUTH": "Bearer sk_YOUR_API_KEY" + } + } + } +} +``` + +Replace `sk_YOUR_API_KEY` with your actual API key. + + + Claude Desktop uses `mcp-remote` to bridge to the SuperDocs Streamable HTTP server. This requires Node.js installed on your machine. + + +If you already have other MCP servers configured, add the `"superdocs"` entry inside the existing `"mcpServers"` object. + +## 3. Restart Claude Desktop + +Close and reopen Claude Desktop. The SuperDocs tools will appear in the tools menu. + +## 4. Test it + +Type in Claude Desktop: + +> "Use the SuperDocs health tool to check if the API is running" + +You should see a response confirming the API is healthy. + +## Example usage + +> "Create a new NDA document for a consulting engagement using SuperDocs" + +> "Search my SuperDocs session for any mentions of payment terms" + +See [Available Tools](/mcp/available-tools) for the full list of what you can do. + + +# Cursor & VS Code +Source: https://docs.superdocs.app/mcp/cursor-vscode + +Connect SuperDocs to Cursor, VS Code, or Windsurf via MCP configuration. + +# Cursor, VS Code & Windsurf + +Connect SuperDocs to your code editor for AI-powered document editing alongside your development workflow. + +## Cursor + +### 1. Create the config file + +Create `.cursor/mcp.json` in your project root (or global config): + +```json .cursor/mcp.json theme={null} +{ + "mcpServers": { + "superdocs": { + "url": "https://api.superdocs.app/mcp/", + "transport": "streamable-http", + "headers": { + "Authorization": "Bearer sk_YOUR_API_KEY" + } + } + } +} +``` + +### 2. Restart Cursor + +The SuperDocs tools will appear in Cursor's AI capabilities. Tools load at startup; if the tools panel says "Loading tools" for more than 10 seconds, restart Cursor again. + +## VS Code + +### 1. Create the config file + +VS Code reads MCP servers from a dedicated `mcp.json` file, not `settings.json`. Create `.vscode/mcp.json` in your workspace, or open the user-level file (shared across all workspaces) from the Command Palette (`Cmd+Shift+P` / `Ctrl+Shift+P`) with **MCP: Open User Configuration**. You can also run **MCP: Add Server** and pick HTTP. + +```json .vscode/mcp.json theme={null} +{ + "servers": { + "superdocs": { + "type": "http", + "url": "https://api.superdocs.app/mcp/", + "headers": { + "Authorization": "Bearer sk_YOUR_API_KEY" + } + } + } +} +``` + +Note the shape: VS Code uses a top-level `servers` key and a `type` field (`"http"` for SuperDocs), unlike Cursor's `mcpServers` + `transport`. + +### 2. Start the server + +Save the file, then start the server from the inline hint VS Code shows in `mcp.json` (or run **MCP: List Servers** and start it there). The tools appear in Copilot's agent mode. If SuperDocs doesn't show up, check the MCP server status in the Output panel, or restart VS Code. + +## Windsurf + +Add the same configuration as Cursor to Windsurf's MCP settings: + +```json theme={null} +{ + "mcpServers": { + "superdocs": { + "url": "https://api.superdocs.app/mcp/", + "transport": "streamable-http", + "headers": { + "Authorization": "Bearer sk_YOUR_API_KEY" + } + } + } +} +``` + +## Test your connection + +In your editor's AI chat, type: + +> "Check the SuperDocs API health" + +If configured correctly, the AI will call the `health` tool and report the API status. + + +# SuperDocs Plugin +Source: https://docs.superdocs.app/mcp/plugin + +The official Claude Code plugin GÇö single install bundles 38 MCP tools, 4 workflow prompts, and the auto-loading skill. + +# SuperDocs Plugin for Claude Code + +The fastest way to install SuperDocs in Claude Code. One command, one prompt for your API key, three things installed at once: the MCP server, the workflow prompts, and the skill. + +## What you get + +| Component | Count | What it does | +| -------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **MCP tools** | 38 | Chat, structural editing, attachments, sessions, multi-document, durable Files, cross-session memory, revert, jobs, templates, pre-signed upload/download, image upload, account status | +| **Workflow prompts** | 4 | `/superdocs:draft_from_outline`, `/superdocs:edit_styled_docx`, `/superdocs:convert_format`, `/superdocs:review_contract_for_redflags` | +| **Skill** | 1 | Auto-loads when you ask Claude to edit, draft, or export documents GÇö no need to think about which tool to use | + +## Install + +Two steps: add the SuperDocs marketplace, then install the plugin from it. + +```bash theme={null} +claude plugin marketplace add superdocsapp/superdocs-plugin +claude plugin install superdocs@superdocs-plugin +``` + +The first command registers the marketplace (the `superdocsapp/superdocs-plugin` GitHub repo); the second installs the `superdocs` plugin from it. The install argument is `plugin-name@marketplace-name` (`superdocs@superdocs-plugin`). Running `claude plugin install superdocsapp/superdocs-plugin` on its own fails with "doesn't exist in marketplace", because that GitHub repo path is only valid for `marketplace add`, not for `install`. + + + **Prefer just the MCP server, or hit a marketplace error?** You can always connect the server directly without the plugin, in one command with no marketplace step: + + ```bash theme={null} + claude mcp add -s user --transport http superdocs https://api.superdocs.app/mcp/ \ + --header "Authorization: Bearer sk_YOUR_API_KEY" + ``` + + This installs the MCP tools only (not the prompts or skill). See [Claude Code GåÆ Manual setup](/mcp/claude-code#manual-setup-advanced). + + +You'll be prompted for your `sk_GǪ` API key. Get one from [use.superdocs.app](https://use.superdocs.app) GåÆ **Settings GåÆ API Keys GåÆ Create**. Free plan includes 500 AI operations per month. + +The key is stored in your OS keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service) GÇö never in plaintext config. + +## Verify + +```bash theme={null} +claude plugin list +``` + +You should see `superdocs` in the list. Then in a Claude Code session, type `/` and you'll see the 4 workflow prompts in the slash menu. + +To test the connection: + +> "Use the SuperDocs health tool to check the API" + +If you see `{"status": "healthy"}`, everything works. + +## Use it + +After install, just ask Claude to do document work. The skill auto-loads and Claude reaches for SuperDocs: + +``` +"Edit /Users/me/contracts/draft.docx GÇö make all H2 headings bold and add a new section at the end called 'Approvals'." +``` + +``` +"Draft an NDA between Acme Corp and Globex Industries, governed by Delaware law, 2-year term." +``` + +Or invoke a workflow prompt directly: + +``` +/superdocs:draft_from_outline +``` + +## Updates + +The plugin auto-updates when a new version is published. To pull the latest immediately: + +```bash theme={null} +claude plugin update superdocs +``` + +To refresh the marketplace listing first (e.g. if a new version isn't showing up yet): + +```bash theme={null} +claude plugin marketplace update superdocs-plugin +claude plugin update superdocs +``` + +To uninstall: + +```bash theme={null} +claude plugin uninstall superdocs +``` + +## Troubleshooting + +**"No commands match `/superdocs:`"** GÇö The plugin didn't load. Check `claude plugin list` and reinstall if missing. Restart Claude Code (the plugin loads at session start). + +**"401 Unauthorized" on tool calls** GÇö API key is wrong or revoked. Reinstall the plugin or update the key with `claude plugin config superdocs api_key`. + +**Tools work but skill doesn't auto-load** GÇö The skill loads probabilistically based on your prompt phrasing. If you want to force it, mention "use SuperDocs" explicitly. + +**Stale prompt content after a SuperDocs backend update** GÇö Run `claude plugin update superdocs` to pull the latest plugin version. + +## What's bundled vs. backend-served + +The plugin itself is small GÇö just the manifest + the skill markdown. The 38 tools and 4 prompts are served live from `https://api.superdocs.app/mcp/` (Streamable HTTP, sk\_ Bearer auth). Tool descriptions, prompt templates, and behavior changes ship from the backend instantly GÇö no plugin update needed for those. + +The plugin's job is to make install painless. The smarts live on the server. + +## Running autonomously? + +If an AI agent is driving this plugin with no human present, it can self-serve its own account and API key in one request, then hand the account to a human near the quota cap using a one-time takeover code. See [For AI Agents](/introduction/agent-signup). + +## Source + +The plugin is open source under MIT. Browse, clone, fork, or report issues at [github.com/superdocsapp/superdocs-plugin](https://github.com/superdocsapp/superdocs-plugin). + +## Manual install (without the plugin) + +For cases where you can't use plugins (older Claude Code, project-scoped `.mcp.json` shared via git, etc.), see the [Claude Code manual setup](/mcp/claude-code#manual-setup-advanced) section. + + +# MCP Setup +Source: https://docs.superdocs.app/mcp/setup + +Connect AI tools like Claude Desktop, Cursor, and VS Code to SuperDocs using the Model Context Protocol (MCP). + +# MCP Integration + + + **New to MCP?** The [Model Context Protocol](https://modelcontextprotocol.io) is an open standard that lets AI assistants (Claude, Cursor, VS Code Copilot, and many others) call external tools and services. You add one server entry to your AI tool's config file, and the tool discovers that server's capabilities automatically. No SDK, no API client code, nothing to build. + + +The Model Context Protocol (MCP) lets AI tools connect to external services. SuperDocs exposes an MCP server with 38 tools and 4 user-invocable workflow prompts for document editing, session management, attachments, multi-document and durable Files, revert, and more GÇö all on a single endpoint at `https://api.superdocs.app/mcp/`. + +When connected, SuperDocs tools appear natively in your AI tool GÇö no API client code needed. + + + **Running as an autonomous AI agent?** You can create your own account and API key in a single request (no web form, no human), then check your quota anytime with the `get_account_status` tool. When you near your monthly cap, hand the account to a human with a one-time **takeover code**: they open an emailed link, enter the code, and adopt the account in place so your work and API key keep working. Full flow (signup, quota, handoff, adopt) is on the [For AI Agents](/introduction/agent-signup) page. + + +## Where to find SuperDocs + +You can install SuperDocs from any of these public channels (most users only need one): + +| Channel | What it gives you | Install | +| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| **Claude Code plugin** (recommended for Claude Code) | MCP server + 4 workflow prompts + auto-loading skill in one command | `claude plugin marketplace add superdocsapp/superdocs-plugin && claude plugin install superdocs@superdocs-plugin` | +| **Official MCP Registry** | Streamable HTTP endpoint discovered automatically by VS Code Copilot `@mcp` search, GitHub MCP Registry, Goose Extensions, PulseMCP, Glama and other MCP-aware tools | Auto-pulled GÇö no action needed in supported clients (search `superdocs` in your tool's MCP catalog) | +| **skills.sh** (cross-agent skill registry) | Auto-loading skill that works with Cursor, GitHub Copilot, Gemini CLI, Cline, Codex, Continue, Goose, Windsurf, Amp, and more | `npx skills add superdocsapp/superdocs-plugin` | +| **Manual MCP config** (any MCP client) | Direct endpoint connection via your tool's MCP config file | See per-client setup below | + +Two conveniences tie these channels together: + +* The plugin and the skill both include a "Setup check" snippet that tells your AI agent to add the MCP server itself (once per session, not per tool call) if it's not already connected. Installing the skill alone is enough to get a working integration on any agent that can run shell commands. +* The reverse also works: connecting the MCP server alone surfaces a recommendation to load the skill, via the MCP `instructions` field returned during the protocol handshake. + +## Prerequisites + +* A SuperDocs account ([sign up free](https://use.superdocs.app)) +* An API key (see [API Keys](/account/api-keys), or generate one from the web app's MCP tab GÇö [MCP Key from Settings](/account/mcp-setup)) + +## Connection details + +| Field | Value | +| --------------- | --------------------------------------- | +| **Endpoint** | `https://api.superdocs.app/mcp/` | +| **Transport** | Streamable HTTP | +| **Auth header** | `Authorization: Bearer sk_YOUR_API_KEY` | + +Both `/mcp` and `/mcp/` are accepted; the trailing-slash form is the canonical spelling used throughout these docs. + + + **Keep your key out of dotfiles and repos.** MCP config files (`.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`) are easy to commit by accident. Where your client supports environment-variable references in its MCP config (for example `${env:SUPERDOCS_API_KEY}` in VS Code's `mcp.json`), prefer that over pasting the raw `sk_` key inline GÇö and never commit a config containing a real key. If a key does leak, revoke it in [API Keys](/account/api-keys) and create a new one. + + +## Supported clients + + + + **One install command GÇö plugin bundles tools + prompts + skill** + + + + Add to claude\_desktop\_config.json + + + + Add to .cursor/mcp.json or .vscode/mcp.json + + + + + These are just the most popular clients. **Any application with an MCP client** can connect to SuperDocs GÇö including your own custom AI agents, internal business tools, or startup products. If it speaks MCP, it works with SuperDocs. Just point it at the endpoint and auth details above. + + +## Quick test + +After connecting, ask your AI tool: + +> "Use the SuperDocs health tool to check the API status" + +If the connection is working, you'll get back `{"status": "healthy"}`. + + + The MCP `health` tool runs over your authenticated `/mcp/` connection, like every MCP tool, so it doubles as a check that your API key is set up correctly. A `401` here means the key is missing or misconfigured, not that the API is down. If you want a zero-auth status probe, use the REST endpoint instead: `curl https://api.superdocs.app/health` requires no authentication. + + +## Troubleshooting + +**Tools panel says "Loading tools" forever** GÇö restart your AI tool. MCP servers load at startup, so a fresh session is required after adding the server. + +**Connection fails or times out** GÇö verify your API key works: + +```bash theme={null} +curl https://api.superdocs.app/v1/sessions \ + -H "Authorization: Bearer sk_YOUR_KEY" +``` + +A `200` response (with a JSON list of your sessions, possibly empty) confirms the key is valid. A `401` means the key is wrong, revoked, or the `Authorization` header didn't reach the server. + + diff --git a/use-cases/Anirudh7S/research-grant-packet-assembler/requirements.txt b/use-cases/Anirudh7S/research-grant-packet-assembler/requirements.txt new file mode 100644 index 00000000..93b41656 --- /dev/null +++ b/use-cases/Anirudh7S/research-grant-packet-assembler/requirements.txt @@ -0,0 +1,4 @@ +requests +python-dotenv +pypdf +python-docx diff --git a/use-cases/Anirudh7S/research-grant-packet-assembler/sample_grant/budget_justification.txt b/use-cases/Anirudh7S/research-grant-packet-assembler/sample_grant/budget_justification.txt new file mode 100644 index 00000000..a763a533 --- /dev/null +++ b/use-cases/Anirudh7S/research-grant-packet-assembler/sample_grant/budget_justification.txt @@ -0,0 +1,25 @@ +BUDGET JUSTIFICATION + +Personnel + +Support is requested for research personnel responsible for participant coordination, data collection, data-quality review, and follow-up activities. + +Principal Investigator + +Dr. Ananya Sharma will provide scientific oversight, supervise the research team, oversee study implementation, and lead analysis planning. + +Co-Investigator + +Dr. Rahul Verma will coordinate community implementation, participating sites, participant engagement, and follow-up evaluation. + +Participant and Site Activities + +Funds are requested for community-site activities, participant communication, screening materials, and implementation support. + +Data Management + +Funds are requested for secure data management, quality-control procedures, and preparation of de-identified analytical datasets. + +Dissemination + +Funds are requested for preparation of research outputs and dissemination of findings to relevant research and community audiences. \ No newline at end of file diff --git a/use-cases/Anirudh7S/research-grant-packet-assembler/sample_grant/data_management_plan.txt b/use-cases/Anirudh7S/research-grant-packet-assembler/sample_grant/data_management_plan.txt new file mode 100644 index 00000000..df3fd489 --- /dev/null +++ b/use-cases/Anirudh7S/research-grant-packet-assembler/sample_grant/data_management_plan.txt @@ -0,0 +1,34 @@ +DATA MANAGEMENT PLAN + +Project: Community-Based Early Detection of Cardiovascular Risk + +Principal Investigator: Dr. Ananya Sharma +Co-Investigator: Dr. Rahul Verma + +Data Types + +The project will collect participant demographic information, screening measurements, referral information, follow-up status, and implementation observations. + +Data Collection + +Data will be collected using standardized electronic forms. Research staff will review submitted records for completeness and resolve data-entry errors through documented correction procedures. + +Data Storage + +Research data will be stored in access-controlled project storage. Access will be limited to authorized members of the research team. + +Data Security + +Participant information will be protected through access controls and appropriate security procedures. Identifying information will not be included in analytical datasets when it is not required for analysis. + +Data Quality + +The research team will conduct periodic quality checks to identify missing, inconsistent, or duplicate records. Corrections will be documented. + +Data Sharing + +De-identified datasets may be shared with qualified researchers when permitted by the study protocol, applicable agreements, and institutional requirements. + +Data Retention + +Project data will be retained according to applicable institutional policies and funder requirements. \ No newline at end of file diff --git a/use-cases/Anirudh7S/research-grant-packet-assembler/sample_grant/facilities_statement.txt b/use-cases/Anirudh7S/research-grant-packet-assembler/sample_grant/facilities_statement.txt new file mode 100644 index 00000000..31175fb8 --- /dev/null +++ b/use-cases/Anirudh7S/research-grant-packet-assembler/sample_grant/facilities_statement.txt @@ -0,0 +1,11 @@ +FACILITIES STATEMENT + +Project: Community-Based Early Detection of Cardiovascular Risk + +The project will use participating community health sites for participant recruitment, screening, referral coordination, and follow-up activities. + +Participating sites will provide appropriate space for participant interactions and screening activities. The research team will use secure project systems for data collection and storage. + +The institutional research environment provides access to administrative support, research personnel, data-management resources, and analytical support required for the proposed project. + +Dr. Ananya Sharma will oversee research operations and study governance. Dr. Rahul Verma will coordinate community implementation and participating sites. \ No newline at end of file diff --git a/use-cases/Anirudh7S/research-grant-packet-assembler/sample_grant/investigator_1_cv.txt b/use-cases/Anirudh7S/research-grant-packet-assembler/sample_grant/investigator_1_cv.txt new file mode 100644 index 00000000..23243657 --- /dev/null +++ b/use-cases/Anirudh7S/research-grant-packet-assembler/sample_grant/investigator_1_cv.txt @@ -0,0 +1,34 @@ +CURRICULUM VITAE + +Dr. Ananya Sharma + +Current Position +Principal Investigator and Associate Professor, Department of Public Health + +Education +Ph.D. in Epidemiology, University of Michigan, 2014 +M.P.H. in Public Health, University of Delhi, 2010 +B.Sc. in Biology, University of Delhi, 2008 + +Research Interests +Community cardiovascular screening +Preventive health interventions +Health-services implementation +Population health analytics + +Professional Experience +2018–Present: Associate Professor, Department of Public Health +2014–2018: Assistant Professor, Department of Public Health +2012–2014: Research Scientist, Center for Population Health + +Selected Publications +Sharma A, Patel R. Community-based cardiovascular risk screening. Journal of Preventive Health. 2023. +Sharma A, Kumar S. Improving preventive-service referral completion. Public Health Research. 2021. +Sharma A, Lee J. Implementation approaches for community screening. Health Services Review. 2019. + +Awards +Outstanding Researcher Award, Department of Public Health, 2023 +Community Health Innovation Award, 2021 + +Current Grant Role +Principal Investigator — Community-Based Early Detection of Cardiovascular Risk \ No newline at end of file diff --git a/use-cases/Anirudh7S/research-grant-packet-assembler/sample_grant/investigator_2_cv.txt b/use-cases/Anirudh7S/research-grant-packet-assembler/sample_grant/investigator_2_cv.txt new file mode 100644 index 00000000..b15d99ea --- /dev/null +++ b/use-cases/Anirudh7S/research-grant-packet-assembler/sample_grant/investigator_2_cv.txt @@ -0,0 +1,35 @@ +ACADEMIC CV + +Dr. Rahul Verma + +Professional Role +Co-Investigator and Assistant Professor, Department of Community Health + +Academic Training +Ph.D., Health Policy and Management, Johns Hopkins University, 2016 +M.P.H., University of Delhi, 2012 +B.A., Sociology, University of Delhi, 2009 + +Research Focus +Community health implementation +Participant engagement +Healthcare referral systems +Program evaluation + +Employment History +2019–Present — Assistant Professor, Department of Community Health +2016–2019 — Research Fellow, Center for Health Policy +2014–2016 — Program Evaluation Associate, Community Health Institute + +Selected Research +Verma R, Singh P. Community engagement in preventive health programs. Community Health Journal. 2024. +Verma R, Shah N. Referral systems and preventive-care access. Health Policy Review. 2022. +Verma R, Gupta M. Evaluating community health interventions. Implementation Science Reports. 2020. + +Honors +Early Career Investigator Award, 2024 +Excellence in Community Research Award, 2022 + +Grant Responsibility +Co-Investigator — Community-Based Early Detection of Cardiovascular Risk +Responsible for community implementation, site coordination, and participant follow-up evaluation. \ No newline at end of file diff --git a/use-cases/Anirudh7S/research-grant-packet-assembler/sample_grant/research_narrative.txt b/use-cases/Anirudh7S/research-grant-packet-assembler/sample_grant/research_narrative.txt new file mode 100644 index 00000000..e1038dee --- /dev/null +++ b/use-cases/Anirudh7S/research-grant-packet-assembler/sample_grant/research_narrative.txt @@ -0,0 +1,51 @@ +RESEARCH NARRATIVE + +Project Title: Community-Based Early Detection of Cardiovascular Risk + +Principal Investigator: +Dr. Ananya Sharma + +Co-Investigator: +Dr. Rahul Verma + +Project Summary + +Cardiovascular disease remains a major public health challenge, particularly in communities where access to preventive screening is limited. This project proposes a community-based screening and early-intervention program designed to identify adults at elevated cardiovascular risk and connect them with appropriate preventive services. + +The project will combine community outreach, structured risk assessment, referral pathways, and follow-up monitoring. The research team will evaluate whether a community-based approach can improve early identification of cardiovascular risk and increase the proportion of participants who receive appropriate follow-up care. + +Specific Aim 1 + +We will establish a standardized cardiovascular risk-screening workflow at participating community sites. The workflow will include participant registration, collection of demographic and clinical information, risk assessment, and referral recommendations. + +Specific Aim 2 + +We will evaluate whether structured follow-up improves completion of recommended preventive services. Participants identified as being at elevated risk will receive follow-up communication and referral support. + +Specific Aim 3 + +We will analyze implementation outcomes across participating sites and identify operational factors associated with successful screening and referral. + +Research Approach + +Dr. Ananya Sharma will serve as Principal Investigator and will lead study design, participant-safety procedures, research oversight, and analysis planning. Dr. Rahul Verma will serve as Co-Investigator and will lead community implementation, site coordination, and evaluation of participant follow-up. + +The study will use a mixed-methods evaluation. Quantitative measures will include the number of individuals screened, the proportion identified as elevated risk, referral completion rates, and follow-up completion rates. Qualitative interviews with participating staff will be used to understand implementation barriers and facilitators. + +The research team will establish standardized data-collection procedures before participant enrollment begins. Staff will receive training on participant communication, data recording, referral procedures, and privacy protections. + +Data will be reviewed periodically to identify missing information and implementation problems. The team will use these findings to improve operational procedures while maintaining the predefined research protocol. + +Expected Outcomes + +The project is expected to produce a reproducible community-based screening workflow, evidence regarding referral completion, and practical recommendations for organizations implementing cardiovascular-risk screening programs. + +Significance + +The proposed work addresses an important gap between cardiovascular-risk identification and access to preventive services. By combining standardized screening with structured follow-up, the project may provide a scalable model for community organizations and healthcare partners. + +Investigators + +Dr. Ananya Sharma — Principal Investigator + +Dr. Rahul Verma — Co-Investigator \ No newline at end of file