← Blog

How to Build a Claude-Powered Contract Analysis Tool

2026-11-05 · 5 min read · SubToAPI Team

Building a Claude-powered contract analysis tool means combining a document ingestion pipeline with structured prompting so Claude reliably extracts clauses, flags risky terms, and summarizes obligations in a format your application can parse. The core engineering challenge isn't getting Claude to "understand" contracts — it already does that well — it's getting consistent, structured output you can store in a database and surface in a UI without manual review of every response.

This guide walks through the actual architecture: how to chunk and feed contract text, how to prompt for structured extraction, how to validate the output, and how to wire it into an API layer your product can call.

What a contract analysis tool actually needs to do

Before writing prompts, define the output contract (pun intended). Most contract analysis tools need to produce:

Claude is well suited for this because contracts are long, unstructured, and full of legal phrasing that benefits from reasoning rather than pure keyword matching — a regex won't catch "either party may terminate upon thirty (30) days' prior written notice" phrased five different ways across five contracts.

Step 1: Get the contract text into Claude

PDFs and Word docs need to be converted to plain text first. Use a library like pdf-parse or mammoth (for .docx) on the backend before sending anything to the API. Don't try to send raw PDF bytes — extract text, normalize whitespace, and keep page breaks as markers so you can map extracted clauses back to a page number later.

Most contracts fit well within Claude's context window in one pass. For unusually long master service agreements with multiple exhibits, split by section headers rather than arbitrary character counts — this keeps clauses intact instead of cutting them mid-sentence.

Step 2: Design a structured extraction prompt

The key to a reliable tool is forcing structured output. Ask Claude to return JSON matching a fixed schema, and be explicit about what happens when a clause is missing.

const systemPrompt = `You are a contract analysis assistant. Extract the following
fields from the contract text and return ONLY valid JSON matching this schema:

{
  "parties": ["string"],
  "effective_date": "string or null",
  "termination_clause": { "text": "string or null", "notice_period_days": "number or null" },
  "liability_cap": { "text": "string or null", "amount": "string or null" },
  "auto_renewal": "boolean",
  "governing_law": "string or null",
  "risk_flags": [{ "clause": "string", "reason": "string", "severity": "low|medium|high" }],
  "summary": "string"
}

If a field is not present in the contract, use null. Do not guess values.
Do not include any text outside the JSON object.`;

This explicit "do not guess" instruction matters. Contract analysis is a domain where hallucinated dollar amounts or made-up notice periods are worse than no answer at all.

Step 3: Call the API and validate the response

Once you have the extraction schema, calling Claude follows the standard messages pattern. If you're using SubToAPI as your API layer, the request looks like this:

curl https://api.subtoapi.app/v1/messages \
  -H "Authorization: Bearer $SUBTOAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-sonnet-4",
    "max_tokens": 2048,
    "system": "You are a contract analysis assistant...",
    "messages": [
      { "role": "user", "content": "CONTRACT TEXT:\n\n" + contractText }
    ]
  }'

Full request and response details are in the messages docs. Always validate the returned JSON with a schema validator (Zod, ajv, or similar) before storing it — if Claude wraps the JSON in markdown fences or adds a sentence of preamble despite instructions, strip that in a post-processing step rather than failing the whole request.

import { z } from "zod";

const ContractSchema = z.object({
  parties: z.array(z.string()),
  effective_date: z.string().nullable(),
  termination_clause: z.object({
    text: z.string().nullable(),
    notice_period_days: z.number().nullable(),
  }),
  risk_flags: z.array(
    z.object({ clause: z.string(), reason: z.string(), severity: z.enum(["low", "medium", "high"]) })
  ),
  summary: z.string(),
});

const parsed = ContractSchema.parse(JSON.parse(cleanedResponse));

Step 4: Handle long contracts and risk comparison

For risk flagging, give Claude a reference baseline instead of asking it to decide what's "risky" in isolation. Include a short description of your organization's standard terms in the system prompt — for example, "our standard liability cap is 12 months of fees paid; flag any cap below that." This turns a subjective judgment into a comparison task, which produces far more consistent results across different contracts.

If you're processing contracts in bulk (a batch of vendor agreements, for instance), queue requests rather than firing them all at once. Streaming isn't necessary for structured JSON extraction — it adds complexity without benefit here, since you need the complete, valid JSON object before you can parse it. Save streaming for interactive features like a chat interface where a user asks follow-up questions about a specific clause.

Step 5: Add a Q&A layer on top of extracted data

Once clauses are extracted and stored, many teams add a secondary feature: letting users ask natural-language questions about a specific contract ("what happens if we miss a payment?"). This is a second, separate Claude call that takes the original contract text plus the user's question — don't try to answer questions purely from the extracted JSON, since nuance gets lost in summarization.

Production considerations

If you're prototyping this as an internal tool or a client-facing SaaS feature, getting an API key and testing against the quickstart takes a few minutes, and the pricing page covers what a Solo or Team plan costs once you move past the free trial.

Questions

Does Claude read contracts from PDF files directly? No — convert the PDF to plain text first using a library like pdf-parse, then send the extracted text to Claude. Sending raw PDF bytes won't work with the messages API.

How do I stop Claude from inventing clause values that aren't in the contract? Be explicit in the system prompt: instruct Claude to use null for missing fields and never guess. Pair this with schema validation on your side to catch malformed responses before they reach your database.

Is streaming necessary for contract extraction? Not for structured JSON extraction — you need the full valid object before parsing it. Streaming is more useful for interactive follow-up Q&A features built on top of the extracted data; see the streaming docs if you add that later.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →