---
title: "Ingest Product Specification Docs with Claude Code"
description: "How to ingest product specification documents in a Claude Code pipeline with EnConvert, returning heading-aware JSONL chunks with tables and code blocks intact"
canonical: "https://www.enconvert.com/solutions/ingest-product-specification-docs-with-claude-code"
locale: "en"
---

# How to Ingest Product Specification Documents with Claude Code

A developer building a Claude Code agent needs to ingest product specification documents, including feature specs, API references, requirements documents, and technical design docs, before passing them into an agent’s context or retrieval layer. These documents are dense with structured content: tables, code blocks, and nested sections, and naive token-based splitting breaks across section boundaries, separates tables from their context, and strips the heading provenance that makes retrieval useful. EnConvert’s Ingest endpoint accepts a DOCX or PDF upload and returns JSONL chunks split at H1, H2, and H3 heading boundaries, with a 512 word soft cap and tables and code blocks kept whole even when they exceed the cap. Each chunk carries headings_path, section, chunk_index, word_count, and source_url metadata, ready to pass into a Claude Code agent or vector store.

## Example

```python
import json
import os
import time
import requests
import anthropic

# Step 1: Upload the product specification document to the Ingest endpoint
with open("product-specification.docx", "rb") as f:
    upload_response = requests.post(
        "https://api.enconvert.com/v2/ingest/files",
        headers={"X-API-Key": os.environ["ENCONVERT_API_KEY"]},
        files={"files": f},
    )

if upload_response.status_code == 402:
    raise RuntimeError("Ingest is not on your plan, or the monthly ops allowance is exhausted")
upload_response.raise_for_status()
job_id = upload_response.json()["job_id"]

# Step 2: Poll until the job reaches a terminal state
while True:
    status_response = requests.get(
        f"https://api.enconvert.com/v2/ingest/{job_id}",
        headers={"X-API-Key": os.environ["ENCONVERT_API_KEY"]},
    )
    status_response.raise_for_status()
    job = status_response.json()

    if job["status"] == "completed":
        break
    if job["status"] in {"failed", "canceled"}:
        raise RuntimeError(
            job.get("error_message") or f"Ingest job {job_id} ended with status {job['status']}"
        )
    time.sleep(5)

# Step 3: Download JSONL output from signed output_url (expires after 15 minutes)
jsonl_response = requests.get(job["output_url"])
jsonl_response.raise_for_status()
chunks = [json.loads(line) for line in jsonl_response.text.splitlines() if line.strip()]

# Tables and code blocks are kept whole even when they exceed the 512 word soft cap
# Step 4: Build context block from chunks, preserving heading path and section metadata
context = "\n\n".join(
    f"[section: {c['metadata']['section']} | headings: {' > '.join(c['metadata']['headings_path'])} | chunk: {c['metadata']['chunk_index']} | source: {c['metadata']['source_url']}]\n"
    f"{c.get('content')}"
    for c in chunks[:10]
)

# Step 5: Pass context into a Claude Code agent using the Anthropic SDK messages API
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])

message = client.messages.create(
    model="claude-sonnet-5-5",
    max_tokens=4096,
    system=(
        "Answer questions about the product specification using only the context provided. "
        "Cite the section path and chunk index for every claim. "
        "Quote tables and code blocks exactly as they appear in the context."
    ),
    messages=[
        {
            "role": "user",
            "content": f"Context:\n\n{context}\n\nWhat are the required fields for the create endpoint, and which section defines them?"
        }
    ],
)

for block in message.content:
    if block.type == "text":
        print(block.text)
```

## Output

The Ingest endpoint returns JSONL chunks split at H1, H2, and H3 heading boundaries with a 512 word soft cap, tables and code blocks kept whole even when they exceed the cap, and each chunk carrying headings_path, section, chunk_index, word_count, and source_url. Naive token-based splitting breaks tables mid-row and severs code blocks from their context, and passing the raw document into an agent context inflates token usage without section provenance.

## Get started

[Get free API key — 500 ops per month, no card required.](https://www.enconvert.com/auth?mode=signup)

## Related solutions

- [How to Scrape Wikipedia with LlamaIndex](https://www.enconvert.com/solutions/scrape-wikipedia-with-llamaindex-enconvert.md)
- [How to Ingest Markdown into LangChain with EnConvert](https://www.enconvert.com/solutions/ingest-markdown-into-langchain-with-heading-context.md)
- [Best JigsawStack Alternative for Structured Data Extraction](https://www.enconvert.com/solutions/jigsawstack-alternative-for-structured-data-enconvert.md)
- [How to Ingest Audio Transcripts into Qdrant with EnConvert](https://www.enconvert.com/solutions/ingest-audio-transcripts-into-qdrant-enconvert-api.md)
- [How to Ingest a CSV into pgvector Without Losing Row Context](https://www.enconvert.com/solutions/ingest-csv-into-pgvector-with-row-context-enconvert-api.md)
- [How to Ingest CSV into CrewAI with EnConvert](https://www.enconvert.com/solutions/ingest-csv-into-crewai-with-enconvert.md)
