# Introduction Access millions of U.S. court opinions, statutes, and regulations through SQL, MCP, or API. Midpage provides access to a broad legal database for the United States, including court opinions, federal and state statutes, and regulations. Our data is collected from official court and government sources through a network of custom-built scrapers. Use the [API dashboard](https://app.midpage.ai/developers) at `app.midpage.ai/developers` to create API keys and review usage. ## Data Access Interfaces There are three ways to access the data: | Interface | Description | |-----------|-------------| | **SQL Read Replica** | Direct PostgreSQL access for raw queries and data exploration | | **MCP Agent** | Model Context Protocol integration for AI assistants | | **REST API** | HTTP endpoints for application integration | ## Database Overview The public read replica exposes two schemas: ### `opinions` Schema — Court Opinions | Table | Description | |-------|-------------| | `courts` | Court definitions | | `cases` | Case metadata and docket information | | `docket_entries` | Individual opinion entries | | `citations` | Reporter citations for opinions | | `content` | Full text content in HTML format | | `opinion_relations` | Citation relationships between opinions | ### `laws` Schema — Statutes & Regulations | Table | Description | |-------|-------------| | `collections` | Content collections (U.S. Code, CFR, state codes and registers) | | `documents` | All hierarchical law content — statutes, regulations, constitutions, guidance (titles, chapters, sections as an ltree) | | `version_relations` | Version-history edges between documents (amendments, supersessions) | ## Coverage See the live coverage overview at [app.midpage.ai/coverage/us](https://app.midpage.ai/coverage/us). ## Data Sources - **300+ Home-Built Scrapers** — Our custom scrapers visit court websites 6 times per day to capture new opinions, orders, and filings. - **Official Government Sources** — Statutes and regulations sourced from GovInfo and official state repositories. - **Enhanced Metadata** — Structured extraction of judges, citations, docket numbers, case types, and party information. # Database Replica **Enterprise Feature** Access to a fully isolated, daily-updated database replica is available for enterprise clients. The replica enables you to fully ingest and customize data for your own use cases. It is powered by logical replication through NeonDB Postgres. This is not included with standard API access. Contact to learn more about enterprise access. Connect to the Midpage read replica using PostgreSQL. ## Connection String ```plaintext postgresql://user:password@host:port/database ``` Contact us to obtain your connection credentials. ## Connecting with psql ```bash psql "postgresql://user:password@host:port/database" ``` ## Connecting with Python ```python import psycopg2 conn = psycopg2.connect( host="host", port="port", database="database", user="user", password="password" ) cursor = conn.cursor() cursor.execute("SELECT * FROM cases.courts LIMIT 10") rows = cursor.fetchall() ``` ## Connecting with Node.js ```javascript import { Pool } from 'pg' const pool = new Pool({ connectionString: process.env.DATABASE_URL }) const result = await pool.query('SELECT * FROM cases.courts LIMIT 10') console.log(result.rows) ``` ## Schema Access The database contains two schemas: * **`cases`** — Court opinions, dockets and caselaw * **`laws`** — Statutes and regulations ```sql -- List all tables in cases schema SELECT table_name FROM information_schema.tables WHERE table_schema = 'cases'; -- List all tables in laws schema SELECT table_name FROM information_schema.tables WHERE table_schema = 'laws'; ``` See the [Schema Reference](/guides/schema) for complete table documentation. ## Change Tracking All tables include an `updated_at` column with automatic triggers. This timestamp is updated whenever a row is modified, helping you track changes made on our end. ```sql -- Find recently updated docket entries SELECT id, description, updated_at FROM cases.docket_entries WHERE updated_at > NOW() - INTERVAL '7 days' ORDER BY updated_at DESC; ``` This is useful for incremental syncs—you can query for rows where `updated_at` is greater than your last sync timestamp. ### One case `updated_at` does not cover Incremental sync on `updated_at` catches inserts, edits and deletions — but not _withdrawals_. * A **deletion** sets `deleted_at` and bumps `updated_at`. Your next incremental query sees the row and its tombstone, and you can mark it deleted on your side. * A **withdrawal** — where a case or document stops being part of the published corpus, for example because it was removed at a court's request or found to be a duplicate — removes the row outright. It leaves no tombstone, and because the row is gone there is nothing for an `updated_at` query to return. So a sync built only on `updated_at` will accumulate rows that are no longer in our corpus. Withdrawals are uncommon, but they are not rare enough to ignore: plan on a periodic full-key reconciliation — pull the complete set of IDs for a table and drop anything on your side that is no longer present. ```sql -- Reconciliation key set: every live case we currently publish SELECT id FROM cases.cases WHERE deleted_at IS NULL; ``` Monthly is enough for most consumers. If you need withdrawals reflected faster than that, talk to us — we can tell you which tables actually move. # MCP Integrate Midpage's legal database with AI assistants using the Model Context Protocol. Research opinions, trial filings, dockets, statutes, and regulations through five tools. ## Versioning These docs describe the current MCP contract, `v4`. Midpage uses major versions for tool contract changes that integrations may need to account for, such as output schema changes or newly introduced tools. If your integration depends on fixed tool schemas or output fields, use a pinned version endpoint. Previous version docs and migration notes: * [MCP v3](/documentation/integration/mcp-tools/v3): `https://app.midpage.ai/mcp/v3` * [MCP v2](/documentation/integration/mcp-tools/v2): `https://app.midpage.ai/mcp/v2` * [MCP v1](/documentation/integration/mcp-tools/v1): `https://app.midpage.ai/mcp/v1` * [MCP changelog](/documentation/integration/mcp-tools/changelog) ## Base URL ```text https://app.midpage.ai/mcp/v4 ``` Use this pinned URL for the `v4` contract documented here. Older SSE clients can use `/mcp/v4/sse` with `/mcp/v4/message`. The unversioned URL follows the latest major version and may introduce breaking changes. It serves `v4` today: ```text https://app.midpage.ai/mcp ``` ## Authentication Midpage supports two authentication models for MCP integrations. | Option | Best for | How it works | | ------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | API key | Server-side integrations, internal tools, and quick prototypes | Your integration sends one shared API key with each request. | | OAuth | End-user apps, partner integrations, and multi-tenant products | Each user signs in with their own Midpage account and your client sends that user's access token. | ### 1. API Key Use API key auth for non-interactive access, server-side jobs, or any integration that should run under one shared credential. Generate an API key in the [Developer Portal](https://app.midpage.ai/developers). If you need help, contact . Then send it in your MCP request headers as: ```http Authorization: Bearer ``` ### 2. OAuth Use OAuth when your client should connect each user to their own Midpage account. Each user will need a Midpage account before they can sign in and authorize your client. For most customers, the easiest setup is: 1. Point your client at `https://app.midpage.ai/mcp/v4` 2. Let your MCP or OAuth library follow Midpage's discovery metadata and register a client automatically if it supports dynamic client registration 3. Let the user sign in and approve access when prompted Most MCP clients should not need pre-provisioned OAuth app credentials. Midpage's Clerk auth server supports dynamic client registration, so compatible clients can create their own credentials automatically. If your client cannot do that and needs a pre-provisioned OAuth app, contact Midpage. Need the manual OAuth settings? If your client supports discovery, start there. In most cases, pointing it at `https://app.midpage.ai/mcp/v4` is enough. Midpage publishes protected-resource metadata on `app.midpage.ai`, and that metadata points clients to the Clerk authorization server. The settings below are only for cases where you need to configure the OAuth flow yourself. **Discovery** * MCP protected-resource metadata: `https://app.midpage.ai/.well-known/oauth-protected-resource/mcp` * Clerk OAuth metadata: `https://clerk.midpage.ai/.well-known/oauth-authorization-server` * Clerk OIDC discovery: `https://clerk.midpage.ai/.well-known/openid-configuration` Some older MCP clients expect OAuth authorization-server metadata on the same origin as the MCP server. Midpage currently publishes protected-resource metadata on `app.midpage.ai` and authorization-server metadata on the Clerk domain above, so older clients may need manual configuration using those Clerk URLs. The current metadata advertises: * authorization server: `https://clerk.midpage.ai` * dynamic client registration endpoint: `https://clerk.midpage.ai/oauth/register` * PKCE challenge method: `S256` * resource scopes: `profile`, `email` **Which OAuth flow to use** * Public clients such as desktop apps, mobile apps, browser apps, and local CLIs should use Authorization Code + PKCE (`S256`). Do not embed a `client_secret` in those clients. If you need this mode, Midpage can provision your Clerk OAuth app as a public client. * Confidential server-side apps can use Authorization Code and exchange the code on their backend with either `client_secret_basic` or `client_secret_post`. * If you need refresh tokens for long-lived sessions, request `offline_access`. **Minimal flow** 1. Fetch the protected-resource metadata document and read `authorization_servers`. 2. Fetch the Clerk authorization-server or OIDC metadata document and use the published endpoints. 3. Start an authorization request with `response_type=code`, `client_id`, `redirect_uri`, `state`, and the `resource` value from the protected-resource metadata document. 4. Request `profile email` for MCP access. Add `openid` if your client expects an ID token. Add `offline_access` if you need a refresh token. 5. Public clients must also send `code_challenge` and `code_challenge_method=S256`. 6. Exchange the authorization `code` for tokens at the Clerk token endpoint, again including the same `resource` value. 7. Call the MCP server with `Authorization: Bearer `. **Example authorize request for a public client** ```text GET https://clerk.midpage.ai/oauth/authorize? response_type=code& client_id=YOUR_CLIENT_ID& redirect_uri=https://your-app.com/oauth/callback& resource=https%3A%2F%2Fapp.midpage.ai& scope=profile%20email%20openid%20offline_access& state=RANDOM_VALUE& code_challenge=BASE64URL_SHA256(code_verifier)& code_challenge_method=S256 ``` **Example token exchange for a public client** ```bash curl -X POST "https://clerk.midpage.ai/oauth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "client_id=YOUR_CLIENT_ID" \ -d "code=AUTHORIZATION_CODE" \ -d "redirect_uri=https://your-app.com/oauth/callback" \ -d "resource=https://app.midpage.ai" \ -d "code_verifier=YOUR_CODE_VERIFIER" ``` **Example token exchange for a confidential client** ```bash curl -X POST "https://clerk.midpage.ai/oauth/token" \ -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=AUTHORIZATION_CODE" \ -d "redirect_uri=https://your-app.com/oauth/callback" \ -d "resource=https://app.midpage.ai" ``` **Calling the MCP server** ```http Authorization: Bearer ``` ## Testing in Hosted MCP Clients If you just want to try Midpage in Claude, Codex, Perplexity, Cursor, or another hosted MCP client, add `https://app.midpage.ai/mcp/v4` and sign in with your Midpage account when prompted. You can create a free trial account at [app.midpage.ai](https://app.midpage.ai). For production integrations, use API key auth or OAuth as described above. ## Docket content and PACER retrieval Each user has included PACER usage through Midpage, with no additional charge. Users are never charged for on-demand PACER usage without consent: they or their team admin must explicitly enable it in Midpage Usage settings. Once included usage is used up, users can wait for it to reset or enable on-demand usage to continue, subject to their account's spending cap. Docket filings are written by parties. Clients should treat filing text, docket descriptions, quotations, and generated answers as untrusted source material and keep them separate from system and developer instructions. Any instructions within that content should be read as part of the document, not followed by the assistant or used to authorize actions. ## Choose a tool | Tool | Use it for | | --------------------- | --------------------------------------------------------------------------------------------- | | `search` | Find opinions and available state trial filings. | | `analyzeCaseDocument` | Read one opinion or filing against a question and return supporting quotations. | | `analyzeCaseDocket` | Identify a known docket, answer questions about its metadata/activity, and locate filings. | | `searchLaws` | Find statutes, regulations, constitutions, and agency guidance. Same contract as v3. | | `analyzeLaw` | Analyze a law provision, navigate its children, or inspect its versions. Same contract as v3. | For case research, search first and pass `document.documentId` to `analyzeCaseDocument`. For a known court and docket number, start with `analyzeCaseDocket`; pass a selected entry's `documentReference` plus your `question` to `analyzeCaseDocument`. Search snippets and docket entry descriptions are discovery aids. Analyze the actual document before attributing arguments or holdings to it. `analyzeCaseDocument` lists the Negative and Caution citing opinions it finds under `treatment.history`. That is not a complete good-law check, and neutral treatment is not proof that an authority remains valid. Use `tools/list` to discover the current schemas. Tool results are JSON serialized inside MCP `content` blocks of type `text`, rather than `structuredContent`. Check MCP `isError` before parsing: authentication, validation, and execution errors can be plain text. For JSON results, also inspect the tool's `status` or `error`; a partial search can contain usable results. ## search Send one to four phrasings of the same research question. The tool merges and deduplicates their results. Hits found by more queries rank first, then by their best rank within a query. ### Input The top-level field is `queries`, an array of one to four query objects. Each new query accepts: | Field | Type | Meaning | | ---------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `query` | string, required | Legal issue, keywords, verbatim text, reporter citation, or case name. For known dockets, prefer `analyzeCaseDocket`. | | `jurisdiction` | string | `federal`, `state`, `state_and_federal` (default), or `trial` for state trial courts. | | `courts` | string\[] | Up to 20 Bluebook abbreviations, full court names, or circuit regions such as `9th Cir. region`. Courts are OR-ed. Omit or send `[]` for no court filter. | | `states` | string\[] | State names. Narrows state courts only; federal courts are unaffected. Trial search needs exactly one state, supplied here or implied by named courts. | | `startDate`, `endDate` | string | Inclusive decision/filing date bounds in `YYYY-MM-DD`. Either bound may be omitted; the start cannot follow the end. | | `documentTypes` | string\[] | Exact document types, OR-ed. Omit or send `[]` for `opinion`, `order`, and `judgment`. `["all"]` alone removes the type restriction. | | `publishStatus` | string | `any` (default), `published`, `unpublished`, or `unknown`. Applies only to the opinion-search source; trial results are unchanged. `unknown` selects missing or unknown status. Restrict only when requested. | Filters compose with AND. For example, a state filter and a named court must agree. Unknown fields are rejected. Optional unused filters may be omitted or `null`. The complete `documentTypes` vocabulary is: ```text all, opinion, brief, motion, order, transcript, other, unknown, judgment, memorandum, answer, complaint, declaration, stipulation, exhibit, notice, summons, certificate, letter, report, expert_report, settlement_agreement, procedural ``` Outside state trial courts, search covers opinion documents. Non-opinion types require a state trial search; `all` does not enable federal filing search. Use `analyzeCaseDocket` for federal filings. A filtered miss does not establish that no document exists: classifications and coverage can be incomplete. Retry an empty type-filtered query with `["all"]`; retry an empty named-court trial query once statewide without `courts`, keeping the state and subject. ```json { "queries": [ { "query": "PSLRA safe harbor statements of current fact", "courts": ["9th Cir."] }, { "query": "forward looking statement mixed present historical facts", "courts": ["9th Cir."] } ] } ``` A state trial filing search: ```json { "queries": [ { "query": "SUNTRUST BANK SKIDMORE IRRIGATION", "jurisdiction": "trial", "states": ["Florida"], "documentTypes": ["motion", "order"] } ] } ``` ### Output | Field | Meaning | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `status` | `ok`: queries ran; `partial`: some queries were skipped or a source failed; `error`: the search could not run. An `ok` result can be empty. | | `results` | One merged array of hits, described below. There is no `totalResults` field. | | `nextCursor` | Optional continuation for this merged result set. | | `skipped` | Optional array with `query`, `reason`, `retryable`, and sometimes `nextAction`. Follow the suggested action or repair the input; retry only retryable failures. | | `warnings` | Optional descriptions of empty or narrowed coverage. | | `error` | Optional explanation when the search could not complete. | Each hit has a `case` with `caseId`, `caseName`, `court`, and optionally `docketNumber` and `url`. A readable document has `document.documentId`, `documentType`, and `url`. Opinion metadata is under `document.opinion`: `citation`, `dateDecided` when known, `publishStatus`, `perCuriam` and `enBanc` (each present only when `true`), `precedentialNote` (the court's limiting publication, precedential, or finality caveat, verbatim, when one was extracted; a missing note does not prove the opinion prints none), and `treatment` (`status`, `citationCount`, `negative`, `caution`). For opinions, `caseName` and the name in `citation` are the opinion's short title when known (for example "Tummel v. Milane"), otherwise the docket caption; the reader `url` is still built from the caption. Filing hits can also have `entry` with available `entryNumber` and `dateFiled`; hits without a document may include `entryId`. Opinion hits omit `entry`. `snippet` is optional and is not a verified quotation. Use UUIDs from the returned fields. The numeric ID inside an opinion reader URL is not a `documentId`. Continue with only the returned cursor: ```json { "queries": [{ "cursor": "COPY_NEXT_CURSOR_HERE" }] } ``` Do not combine a cursor with a new query or filters. Cursors expire after 30 minutes. A search exposes at most 100 results across pages; when no cursor remains, refine the query if you need more. An exact federal docket lookup may instead return `skipped[].nextAction` directing you to `analyzeCaseDocket`. ## analyzeCaseDocument Read one opinion or filing against a legal question. The tool returns source-extracted quotations and an answer, rather than the full document text. ### Input | Field | Type | Meaning | | ------------ | ---------------- | ---------------------------------------------------------------------------------------- | | `question` | string, required | What to determine from this document. | | `documentId` | string | Document UUID from search or a docket's `documentReference`. | | `citation` | string | Reporter citation, for example `556 U.S. 662`. WL and LEXIS citations are not supported. | | `entry` | object | `caseId` and `entryNumber`, with optional `attachmentNumber`. | Provide one usable reference. If several are supplied, precedence is `documentId`, then `citation`, then `entry`. Omit or set unused references to `null`. For a main filing, omit `attachmentNumber` or send `null`, `""`, or `"0"`. For an attachment, send its positive integer number as a string, such as `"1"`; `"14-1"` is not an attachment number. ```json { "citation": "556 U.S. 662", "question": "Are threadbare recitals of a cause of action entitled to an assumption of truth?" } ``` To read a docket attachment, copy the case and entry reference returned by docket analysis: ```json { "entry": { "caseId": "8e00e661-a0c3-5c29-9002-1e7eb0d5730d", "entryNumber": "14", "attachmentNumber": "1" }, "question": "What facts does this exhibit establish?" } ``` The attachment example illustrates the input shape, not a claim that this case has that attachment. Use a returned reference for actual retrieval. Stored filings are preferred; a missing copy may require PACER retrieval, subject to the [included usage and consent requirements](#docket-content-and-pacer-retrieval) above. ### Output An `ok` result includes: | Field | Meaning | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `case` | Case identity and reader URL. | | `document` | `documentType`, reader `url`, and `documentId` when known. Opinions may include `opinion` with `citation`, `dateDecided`, `publishStatus`, `perCuriam` and `enBanc` (each present only when `true`), `precedentialNote` when the opinion prints a limiting caveat, and the `treatment` block described below. As in `search`, an opinion's `caseName` and citation use its short title when known. A newly acquired filing may have no document ID; reuse its entry reference. | | `entry` | Filing entry number and available attachment, date, and description fields. Omitted for opinions. | | `answer` | Direct answer, with relevant procedural outcomes and what the source leaves open. | | `supportedPropositions` | Array of `proposition`, verbatim `quote`, `quoteTruncated`, optional `deeplinkUrl`, and optional `opinionSection`. | | `doesNotAddress` | Unanswered parts of the question. Do not cite this source for those points. | | `sourceWarnings` | Optional warnings to resolve before citing: source-status caveats, Negative or material Caution treatment pointing at `treatment.history`, an unverified later history when the citator has no record, or a treatment check that could not run. | Preserve the qualifications in each proposition and any concurrence/dissent or party attribution in `opinionSection`. A proposed order is not an entered ruling. `quoteTruncated: true` means the returned quotation was shortened. Use `deeplinkUrl` when present, otherwise `document.url`, and copy the URL exactly. Do not invent page pinpoints or infer them from PDF page positions. `document.opinion.treatment` summarizes the opinion's later history across every stored copy of it: | Field | Meaning | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `status` | Worst treatment across copies and binding citers: `Negative`, `Caution`, or the citator's status. `unknown` means the citator has not seen the opinion. Neutral is not validation. | | `citationCount` | Citing opinions across all copies. | | `negative`, `caution` | Citing opinions with that treatment. When binding citers exist, each counts distinct binding citers. | | `history` | Negative or Caution citing opinions, up to five: binding courts first, then Negative before Caution, then court rank, then earliest first. Each has `category`, `bindingCourt`, `citation`, `court`, and when known `dateDecided`, `reason`, and a `documentId` for `analyzeCaseDocument`. Empty when the citator found none; omitted when the later history is unknown or could not be read. | | `more` | Binding citers beyond the five listed. Present only when nonzero. | Read the listed citing opinions before relying on a flagged case. Other statuses are `invalid_reference`, `not_found`, `ambiguous`, `text_not_available`, `pacer_fetch_failed`, and `analysis_failed`. Read `message`; `candidates` can support a corrected lookup and `url` can support manual reading. An unread filing must not be represented as analyzed. ## analyzeCaseDocket Answer questions about the case's metadata and recorded activity, including judges, parties, case category, and procedural history. Use it to locate filings, then analyze their contents separately. ### Input | Field | Type | Meaning | | ------------ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `question` | string, required | What to determine from the docket metadata or entries. | | `caseId` | string | Case UUID from search or document analysis. | | `docket` | object | `court` and `docketNumber`, plus optional `caseName` to disambiguate. Courts accept Bluebook abbreviations or full names. | | `forceFetch` | boolean | Omit or use `false` for historical research or finding filings. Use `true` when the user needs the latest official PACER docket. | Provide `caseId` or `docket`; `caseId` takes precedence. `forceFetch` is ignored with a warning when PACER cannot serve the case. V4 can read stored docket material without an upfront premium gate; live PACER acquisition is metered, and an explicit refresh requires premium access. A first acquisition can require PACER even when `forceFetch` is omitted. The same [included usage and consent requirements](#docket-content-and-pacer-retrieval) apply with either setting. ```json { "docket": { "court": "N.D. Ill.", "docketNumber": "1:16-cv-11057" }, "question": "What was filed or entered during the past month?", "forceFetch": true } ``` ### Output | Field | Meaning | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `status` | `ok` on a successful analysis. | | `case` | `caseId`, `caseName`, `court`, reader `url`, and available docket/date metadata. | | `answer` | Answer based on available metadata and recorded entries. Metadata-only answers can have no selected entries. | | `relevantEntries` | Up to 40 entries with `hasDocument` and available `entryNumber`, `attachmentNumber`, `dateFiled`, `description`, and `documentReference`. Stored entries may have `entryId`. | | `doesNotAddress` | Unanswered parts of the question; empty when answered. | | `coverage` | `kind`: `docket_report`, `stored_entries`, or `opinion_only`; `entryCount`: analyzed entries; `truncated`: source or returned selection is incomplete. Read warnings and narrow the question when needed. | | `freshness` | `cached`, `liveFetchSucceeded`, `fetchedAt` (acquisition timestamp or `null`), and `latestEntryDate` (latest recorded entry date or `null`). | | `retrievalSource` | `midpage`, `recap`, or `pacer`. | | `docketReport` | Optional `downloadUrl` and `suggestedFileName` for the report file. | | `warnings` | Optional source and retrieval caveats. | `documentReference` is either `{ "documentId": "..." }` or `{ "entry": { "caseId": "...", "entryNumber": "...", "attachmentNumber": null } }`. Attachments use their number instead of `null`. Pass the object unchanged to `analyzeCaseDocument` and add `question`. A reference indicates an available retrieval path, not a guarantee that fetching will succeed. Use the two report links for different purposes: | Field | Purpose | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `case.url` | Citations and reader links such as **View docket**. Opens the docket reader. | | `docketReport.downloadUrl` | Fetching the original report for download or processing. Link it in an answer only when offering an explicit download. | Copy these URLs exactly. Do not construct raw-download paths from reader URLs or use a download URL for a “View docket” link. A recent acquisition timestamp does not prove the underlying report is current. In particular, a newly acquired RECAP report can be stale. A failed refresh does not establish that no new filings exist. Use `liveFetchSucceeded`, coverage, the requested period, and warnings when describing current status. Other statuses are `invalid_reference`, `not_found`, `ambiguous`, `pacer_fetch_failed`, and `analysis_failed`, each with a `message`. Ambiguous lookups can return `candidates`. An analysis failure can also return a top-level `downloadUrl` for manual reading. ## searchLaws Full-text search over statutes, regulations, constitutions, and agency guidance — US federal plus all 50 states and DC. Use it to FIND the governing provision, then call `analyzeLaw` with the result's `id` before citing it. Results are current law by default (see the [Laws API guide](/documentation/integration/laws-api) for the currency derivation). Inputs are strict: unknown keys are rejected rather than silently ignored. **Parameters:** | Parameter | Type | Required | Description | | ------------------ | --------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `query` | string | Yes | Legal concepts / key terms (BM25 keyword search) | | `states` | string\[] | No | State names or USPS abbreviations (`"California"`, `"TX"`); federal law is `"Federal"`. Validated server-side — unrecognized values error with did-you-mean suggestions | | `collectionTypes` | string\[] | No | `statute`, `regulation`, `constitution`, `guidance`, `executive_order`, `notice` | | `collections` | string\[] | No | Specific codes, by id (e.g. `"cfr"`, `"wa-rcw"`) or human-readable name (e.g. `"Code of Federal Regulations"`, `"Texas Administrative Code"`) — resolved server-side; unresolvable values error with did-you-mean suggestions | | `pathPrefix` | string | No | Subtree filter within one code — a dot-separated ltree path from a previous result (e.g. `"cfr.t21"` = all of 21 CFR, including descendants); pair with `collections`. Malformed prefixes error rather than matching nothing | | `currentOnly` | boolean | No | Default true — only the law in force today | | `page`, `pageSize` | number | No | Pagination (max 50 per page; results beyond the first 10,000 cannot be paginated) | **Returns:** | Field | Type | Description | | ------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `results` | array | Matching provisions: `id`, `citation`, `title`, `number`, jurisdiction and path fields, dates, `isCurrent` / `isHistorical`, `snippets` (matched fragments), and `url` (the Midpage page for that exact version) | | `total` | number | Estimate for the whole query; a page can hold fewer than `pageSize` results | | `stateCounts` | object | Estimated per-state hit counts for the WHOLE query — for state surveys, run one unfiltered query and read this, then verify per state with a filtered search | | `error` | string | Set instead of results when a filter could not be resolved or the pagination window was exceeded — the message says how to fix the call | Never cite from `snippets` — they are relevance context. Call `analyzeLaw` with the result `id` and quote from its verified passages. **Example:** ```json { "query": "telehealth prescribing controlled substances", "collectionTypes": ["regulation"], "states": ["California", "Texas"] } ``` ## analyzeLaw Analyze a statute, regulation, constitutional provision, or agency guidance document against a specific legal question, returning verified verbatim passages — the document text is analyzed internally and raw text is never returned. Resolves to the CURRENT version by default (see the [Laws API guide](/documentation/integration/laws-api) for how currency is derived); container nodes return their children for navigation; versioned provisions return the full version chain with dates. **Parameters** (provide exactly one of the four lookup keys): | Parameter | Type | Required | Description | | ------------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `citation` | string | No\* | Canonical citation (e.g., `"42 U.S.C. § 1983"`, `"Cal. Code Regs. tit. 22, § 51451"`). Matching ignores punctuation, spacing, and `§`/`sec.`/`section` style (`"42 USC 1983"` works), but not abbreviation differences — use the jurisdiction's canonical wording | | `registerCitation` | string | No\* | Federal Register / state register citation (e.g., `"89 FR 12345"`) | | `path` | string | No\* | Full ltree path from a previous result (pair with `collection`) | | `id` | string | No\* | Document UUID from a previous result | | `question` | string | Yes | The legal question to answer about the document | | `collection` | string | No | Disambiguates path/citation/registerCitation lookups — a collection id (e.g., `"cfr"`, `"uscode"`) or human-readable name (e.g., `"Code of Federal Regulations"`); unresolvable values error with did-you-mean suggestions | | `includeHistorical` | boolean | No | Allow superseded / future-effective versions in citation/path/registerCitation lookups (default: current law only); `id` always returns that exact version | \* Provide exactly one of `citation`, `registerCitation`, `path`, or `id` **Returns:** | Field | Type | Description | | ---------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `citation`, `title`, `number` | string | Identification of the provision | | `registerCitation` | string | FR / state-register cite, for regulations that carry one | | `collectionId`, `collectionName` | string | The code the provision belongs to (e.g. `cfr` / "Code of Federal Regulations") | | `effectiveDate`, `publicationDate` | string | Source-stated dates (null when the source states none) | | `analysis` | object | `summary`, `passages` (`point` + verified verbatim `quote` + `deeplinkUrl`, a Midpage link that opens the provision at the quoted passage), `doesNotAddress` — aspects of the question the document does not cover; check it before citing. Absent for containers without their own text | | `children` | array | For containers: up to 250 child provisions (`id`, `number`, `citation`, `title`, `sortOrder`) — navigation only, analyze a child by its `id`; `childrenTotal` is the true count | | `versions` | array | Version chain with dates and `isHistorical` / `isCurrent` / `isRequested` flags (empty without history) | | `parentId` | string | Container node id — look it up to zoom out | | `isCurrent`, `isHistorical` | boolean | Whether this text is the law in force today / whether it has been superseded | | `state`, `collectionType` | string | Jurisdiction (`"Federal"` or state name) and statute/regulation/constitution/guidance/executive\_order/notice | | `url`, `sourceUrl` | string | Midpage link for this exact version (non-current versions carry `?version=`) and official source link | | `ambiguousMatches` | array | When the reference matched several documents — candidate summaries; retry with one match's `id` | | `error` | string | The reference could not be resolved or the provision could not be analyzed — the message says how to proceed (some provisions are review-only via `url`) | **Example:** ```json { "citation": "21 C.F.R. § 820.30", "question": "What design control requirements apply to medical device manufacturers?" } ``` ## Migrate from v3 | V3 | V4 | | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | `search` with opinion-focused filters and per-query results | `search` with case/document identities, exact document types, one merged ranking, one status, and one cursor. | | `analyzeOpinion` and `findInOpinion` | `analyzeCaseDocument` for question-driven opinion or filing analysis. No standalone `findInOpinion` tool. | | `analyzeDocketReport` | `analyzeCaseDocket` for report metadata and recorded activity. | | `analyzeDocketFiling` | `analyzeCaseDocument` with a document UUID or entry reference. | | `searchLaws`, `analyzeLaw` | Same contracts. | V3 docket tools keep their premium-access gate. V4 admits stored material without that upfront gate; actual PACER retrieval remains metered. V4 is not a drop-in replacement: update tool names, reference fields, response parsing, citation links, and pagination together. Keep integrations pinned to their existing version until they are ready to migrate. ## Changelog See the [MCP changelog](/documentation/integration/mcp-tools/changelog) for version history and migration notes. # MCP Integrate Midpage's legal database with AI assistants using the Model Context Protocol. Research opinions, trial filings, dockets, statutes, and regulations through five tools. ## Versioning These docs describe the current MCP contract, `v4`. Midpage uses major versions for tool contract changes that integrations may need to account for, such as output schema changes or newly introduced tools. If your integration depends on fixed tool schemas or output fields, use a pinned version endpoint. Previous version docs and migration notes: * [MCP v3](/documentation/integration/mcp-tools/v3): `https://app.midpage.ai/mcp/v3` * [MCP v2](/documentation/integration/mcp-tools/v2): `https://app.midpage.ai/mcp/v2` * [MCP v1](/documentation/integration/mcp-tools/v1): `https://app.midpage.ai/mcp/v1` * [MCP changelog](/documentation/integration/mcp-tools/changelog) ## Base URL ```text https://app.midpage.ai/mcp/v4 ``` Use this pinned URL for the `v4` contract documented here. Older SSE clients can use `/mcp/v4/sse` with `/mcp/v4/message`. The unversioned URL follows the latest major version and may introduce breaking changes. It serves `v4` today: ```text https://app.midpage.ai/mcp ``` ## Authentication Midpage supports two authentication models for MCP integrations. | Option | Best for | How it works | | ------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | API key | Server-side integrations, internal tools, and quick prototypes | Your integration sends one shared API key with each request. | | OAuth | End-user apps, partner integrations, and multi-tenant products | Each user signs in with their own Midpage account and your client sends that user's access token. | ### 1. API Key Use API key auth for non-interactive access, server-side jobs, or any integration that should run under one shared credential. Generate an API key in the [Developer Portal](https://app.midpage.ai/developers). If you need help, contact . Then send it in your MCP request headers as: ```http Authorization: Bearer ``` ### 2. OAuth Use OAuth when your client should connect each user to their own Midpage account. Each user will need a Midpage account before they can sign in and authorize your client. For most customers, the easiest setup is: 1. Point your client at `https://app.midpage.ai/mcp/v4` 2. Let your MCP or OAuth library follow Midpage's discovery metadata and register a client automatically if it supports dynamic client registration 3. Let the user sign in and approve access when prompted Most MCP clients should not need pre-provisioned OAuth app credentials. Midpage's Clerk auth server supports dynamic client registration, so compatible clients can create their own credentials automatically. If your client cannot do that and needs a pre-provisioned OAuth app, contact Midpage. Need the manual OAuth settings? If your client supports discovery, start there. In most cases, pointing it at `https://app.midpage.ai/mcp/v4` is enough. Midpage publishes protected-resource metadata on `app.midpage.ai`, and that metadata points clients to the Clerk authorization server. The settings below are only for cases where you need to configure the OAuth flow yourself. **Discovery** * MCP protected-resource metadata: `https://app.midpage.ai/.well-known/oauth-protected-resource/mcp` * Clerk OAuth metadata: `https://clerk.midpage.ai/.well-known/oauth-authorization-server` * Clerk OIDC discovery: `https://clerk.midpage.ai/.well-known/openid-configuration` Some older MCP clients expect OAuth authorization-server metadata on the same origin as the MCP server. Midpage currently publishes protected-resource metadata on `app.midpage.ai` and authorization-server metadata on the Clerk domain above, so older clients may need manual configuration using those Clerk URLs. The current metadata advertises: * authorization server: `https://clerk.midpage.ai` * dynamic client registration endpoint: `https://clerk.midpage.ai/oauth/register` * PKCE challenge method: `S256` * resource scopes: `profile`, `email` **Which OAuth flow to use** * Public clients such as desktop apps, mobile apps, browser apps, and local CLIs should use Authorization Code + PKCE (`S256`). Do not embed a `client_secret` in those clients. If you need this mode, Midpage can provision your Clerk OAuth app as a public client. * Confidential server-side apps can use Authorization Code and exchange the code on their backend with either `client_secret_basic` or `client_secret_post`. * If you need refresh tokens for long-lived sessions, request `offline_access`. **Minimal flow** 1. Fetch the protected-resource metadata document and read `authorization_servers`. 2. Fetch the Clerk authorization-server or OIDC metadata document and use the published endpoints. 3. Start an authorization request with `response_type=code`, `client_id`, `redirect_uri`, `state`, and the `resource` value from the protected-resource metadata document. 4. Request `profile email` for MCP access. Add `openid` if your client expects an ID token. Add `offline_access` if you need a refresh token. 5. Public clients must also send `code_challenge` and `code_challenge_method=S256`. 6. Exchange the authorization `code` for tokens at the Clerk token endpoint, again including the same `resource` value. 7. Call the MCP server with `Authorization: Bearer `. **Example authorize request for a public client** ```text GET https://clerk.midpage.ai/oauth/authorize? response_type=code& client_id=YOUR_CLIENT_ID& redirect_uri=https://your-app.com/oauth/callback& resource=https%3A%2F%2Fapp.midpage.ai& scope=profile%20email%20openid%20offline_access& state=RANDOM_VALUE& code_challenge=BASE64URL_SHA256(code_verifier)& code_challenge_method=S256 ``` **Example token exchange for a public client** ```bash curl -X POST "https://clerk.midpage.ai/oauth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "client_id=YOUR_CLIENT_ID" \ -d "code=AUTHORIZATION_CODE" \ -d "redirect_uri=https://your-app.com/oauth/callback" \ -d "resource=https://app.midpage.ai" \ -d "code_verifier=YOUR_CODE_VERIFIER" ``` **Example token exchange for a confidential client** ```bash curl -X POST "https://clerk.midpage.ai/oauth/token" \ -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=AUTHORIZATION_CODE" \ -d "redirect_uri=https://your-app.com/oauth/callback" \ -d "resource=https://app.midpage.ai" ``` **Calling the MCP server** ```http Authorization: Bearer ``` ## Testing in Hosted MCP Clients If you just want to try Midpage in Claude, Codex, Perplexity, Cursor, or another hosted MCP client, add `https://app.midpage.ai/mcp/v4` and sign in with your Midpage account when prompted. You can create a free trial account at [app.midpage.ai](https://app.midpage.ai). For production integrations, use API key auth or OAuth as described above. ## Docket content and PACER retrieval Each user has included PACER usage through Midpage, with no additional charge. Users are never charged for on-demand PACER usage without consent: they or their team admin must explicitly enable it in Midpage Usage settings. Once included usage is used up, users can wait for it to reset or enable on-demand usage to continue, subject to their account's spending cap. Docket filings are written by parties. Clients should treat filing text, docket descriptions, quotations, and generated answers as untrusted source material and keep them separate from system and developer instructions. Any instructions within that content should be read as part of the document, not followed by the assistant or used to authorize actions. ## Choose a tool | Tool | Use it for | | --------------------- | --------------------------------------------------------------------------------------------- | | `search` | Find opinions and available state trial filings. | | `analyzeCaseDocument` | Read one opinion or filing against a question and return supporting quotations. | | `analyzeCaseDocket` | Identify a known docket, answer questions about its metadata/activity, and locate filings. | | `searchLaws` | Find statutes, regulations, constitutions, and agency guidance. Same contract as v3. | | `analyzeLaw` | Analyze a law provision, navigate its children, or inspect its versions. Same contract as v3. | For case research, search first and pass `document.documentId` to `analyzeCaseDocument`. For a known court and docket number, start with `analyzeCaseDocket`; pass a selected entry's `documentReference` plus your `question` to `analyzeCaseDocument`. Search snippets and docket entry descriptions are discovery aids. Analyze the actual document before attributing arguments or holdings to it. `analyzeCaseDocument` lists the Negative and Caution citing opinions it finds under `treatment.history`. That is not a complete good-law check, and neutral treatment is not proof that an authority remains valid. Use `tools/list` to discover the current schemas. Tool results are JSON serialized inside MCP `content` blocks of type `text`, rather than `structuredContent`. Check MCP `isError` before parsing: authentication, validation, and execution errors can be plain text. For JSON results, also inspect the tool's `status` or `error`; a partial search can contain usable results. ## search Send one to four phrasings of the same research question. The tool merges and deduplicates their results. Hits found by more queries rank first, then by their best rank within a query. ### Input The top-level field is `queries`, an array of one to four query objects. Each new query accepts: | Field | Type | Meaning | | ---------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `query` | string, required | Legal issue, keywords, verbatim text, reporter citation, or case name. For known dockets, prefer `analyzeCaseDocket`. | | `jurisdiction` | string | `federal`, `state`, `state_and_federal` (default), or `trial` for state trial courts. | | `courts` | string\[] | Up to 20 Bluebook abbreviations, full court names, or circuit regions such as `9th Cir. region`. Courts are OR-ed. Omit or send `[]` for no court filter. | | `states` | string\[] | State names. Narrows state courts only; federal courts are unaffected. Trial search needs exactly one state, supplied here or implied by named courts. | | `startDate`, `endDate` | string | Inclusive decision/filing date bounds in `YYYY-MM-DD`. Either bound may be omitted; the start cannot follow the end. | | `documentTypes` | string\[] | Exact document types, OR-ed. Omit or send `[]` for `opinion`, `order`, and `judgment`. `["all"]` alone removes the type restriction. | | `publishStatus` | string | `any` (default), `published`, `unpublished`, or `unknown`. Applies only to the opinion-search source; trial results are unchanged. `unknown` selects missing or unknown status. Restrict only when requested. | Filters compose with AND. For example, a state filter and a named court must agree. Unknown fields are rejected. Optional unused filters may be omitted or `null`. The complete `documentTypes` vocabulary is: ```text all, opinion, brief, motion, order, transcript, other, unknown, judgment, memorandum, answer, complaint, declaration, stipulation, exhibit, notice, summons, certificate, letter, report, expert_report, settlement_agreement, procedural ``` Outside state trial courts, search covers opinion documents. Non-opinion types require a state trial search; `all` does not enable federal filing search. Use `analyzeCaseDocket` for federal filings. A filtered miss does not establish that no document exists: classifications and coverage can be incomplete. Retry an empty type-filtered query with `["all"]`; retry an empty named-court trial query once statewide without `courts`, keeping the state and subject. ```json { "queries": [ { "query": "PSLRA safe harbor statements of current fact", "courts": ["9th Cir."] }, { "query": "forward looking statement mixed present historical facts", "courts": ["9th Cir."] } ] } ``` A state trial filing search: ```json { "queries": [ { "query": "SUNTRUST BANK SKIDMORE IRRIGATION", "jurisdiction": "trial", "states": ["Florida"], "documentTypes": ["motion", "order"] } ] } ``` ### Output | Field | Meaning | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `status` | `ok`: queries ran; `partial`: some queries were skipped or a source failed; `error`: the search could not run. An `ok` result can be empty. | | `results` | One merged array of hits, described below. There is no `totalResults` field. | | `nextCursor` | Optional continuation for this merged result set. | | `skipped` | Optional array with `query`, `reason`, `retryable`, and sometimes `nextAction`. Follow the suggested action or repair the input; retry only retryable failures. | | `warnings` | Optional descriptions of empty or narrowed coverage. | | `error` | Optional explanation when the search could not complete. | Each hit has a `case` with `caseId`, `caseName`, `court`, and optionally `docketNumber` and `url`. A readable document has `document.documentId`, `documentType`, and `url`. Opinion metadata is under `document.opinion`: `citation`, `dateDecided` when known, `publishStatus`, `perCuriam` and `enBanc` (each present only when `true`), `precedentialNote` (the court's limiting publication, precedential, or finality caveat, verbatim, when one was extracted; a missing note does not prove the opinion prints none), and `treatment` (`status`, `citationCount`, `negative`, `caution`). For opinions, `caseName` and the name in `citation` are the opinion's short title when known (for example "Tummel v. Milane"), otherwise the docket caption; the reader `url` is still built from the caption. Filing hits can also have `entry` with available `entryNumber` and `dateFiled`; hits without a document may include `entryId`. Opinion hits omit `entry`. `snippet` is optional and is not a verified quotation. Use UUIDs from the returned fields. The numeric ID inside an opinion reader URL is not a `documentId`. Continue with only the returned cursor: ```json { "queries": [{ "cursor": "COPY_NEXT_CURSOR_HERE" }] } ``` Do not combine a cursor with a new query or filters. Cursors expire after 30 minutes. A search exposes at most 100 results across pages; when no cursor remains, refine the query if you need more. An exact federal docket lookup may instead return `skipped[].nextAction` directing you to `analyzeCaseDocket`. ## analyzeCaseDocument Read one opinion or filing against a legal question. The tool returns source-extracted quotations and an answer, rather than the full document text. ### Input | Field | Type | Meaning | | ------------ | ---------------- | ---------------------------------------------------------------------------------------- | | `question` | string, required | What to determine from this document. | | `documentId` | string | Document UUID from search or a docket's `documentReference`. | | `citation` | string | Reporter citation, for example `556 U.S. 662`. WL and LEXIS citations are not supported. | | `entry` | object | `caseId` and `entryNumber`, with optional `attachmentNumber`. | Provide one usable reference. If several are supplied, precedence is `documentId`, then `citation`, then `entry`. Omit or set unused references to `null`. For a main filing, omit `attachmentNumber` or send `null`, `""`, or `"0"`. For an attachment, send its positive integer number as a string, such as `"1"`; `"14-1"` is not an attachment number. ```json { "citation": "556 U.S. 662", "question": "Are threadbare recitals of a cause of action entitled to an assumption of truth?" } ``` To read a docket attachment, copy the case and entry reference returned by docket analysis: ```json { "entry": { "caseId": "8e00e661-a0c3-5c29-9002-1e7eb0d5730d", "entryNumber": "14", "attachmentNumber": "1" }, "question": "What facts does this exhibit establish?" } ``` The attachment example illustrates the input shape, not a claim that this case has that attachment. Use a returned reference for actual retrieval. Stored filings are preferred; a missing copy may require PACER retrieval, subject to the [included usage and consent requirements](#docket-content-and-pacer-retrieval) above. ### Output An `ok` result includes: | Field | Meaning | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `case` | Case identity and reader URL. | | `document` | `documentType`, reader `url`, and `documentId` when known. Opinions may include `opinion` with `citation`, `dateDecided`, `publishStatus`, `perCuriam` and `enBanc` (each present only when `true`), `precedentialNote` when the opinion prints a limiting caveat, and the `treatment` block described below. As in `search`, an opinion's `caseName` and citation use its short title when known. A newly acquired filing may have no document ID; reuse its entry reference. | | `entry` | Filing entry number and available attachment, date, and description fields. Omitted for opinions. | | `answer` | Direct answer, with relevant procedural outcomes and what the source leaves open. | | `supportedPropositions` | Array of `proposition`, verbatim `quote`, `quoteTruncated`, optional `deeplinkUrl`, and optional `opinionSection`. | | `doesNotAddress` | Unanswered parts of the question. Do not cite this source for those points. | | `sourceWarnings` | Optional warnings to resolve before citing: source-status caveats, Negative or material Caution treatment pointing at `treatment.history`, an unverified later history when the citator has no record, or a treatment check that could not run. | Preserve the qualifications in each proposition and any concurrence/dissent or party attribution in `opinionSection`. A proposed order is not an entered ruling. `quoteTruncated: true` means the returned quotation was shortened. Use `deeplinkUrl` when present, otherwise `document.url`, and copy the URL exactly. Do not invent page pinpoints or infer them from PDF page positions. `document.opinion.treatment` summarizes the opinion's later history across every stored copy of it: | Field | Meaning | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `status` | Worst treatment across copies and binding citers: `Negative`, `Caution`, or the citator's status. `unknown` means the citator has not seen the opinion. Neutral is not validation. | | `citationCount` | Citing opinions across all copies. | | `negative`, `caution` | Citing opinions with that treatment. When binding citers exist, each counts distinct binding citers. | | `history` | Negative or Caution citing opinions, up to five: binding courts first, then Negative before Caution, then court rank, then earliest first. Each has `category`, `bindingCourt`, `citation`, `court`, and when known `dateDecided`, `reason`, and a `documentId` for `analyzeCaseDocument`. Empty when the citator found none; omitted when the later history is unknown or could not be read. | | `more` | Binding citers beyond the five listed. Present only when nonzero. | Read the listed citing opinions before relying on a flagged case. Other statuses are `invalid_reference`, `not_found`, `ambiguous`, `text_not_available`, `pacer_fetch_failed`, and `analysis_failed`. Read `message`; `candidates` can support a corrected lookup and `url` can support manual reading. An unread filing must not be represented as analyzed. ## analyzeCaseDocket Answer questions about the case's metadata and recorded activity, including judges, parties, case category, and procedural history. Use it to locate filings, then analyze their contents separately. ### Input | Field | Type | Meaning | | ------------ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `question` | string, required | What to determine from the docket metadata or entries. | | `caseId` | string | Case UUID from search or document analysis. | | `docket` | object | `court` and `docketNumber`, plus optional `caseName` to disambiguate. Courts accept Bluebook abbreviations or full names. | | `forceFetch` | boolean | Omit or use `false` for historical research or finding filings. Use `true` when the user needs the latest official PACER docket. | Provide `caseId` or `docket`; `caseId` takes precedence. `forceFetch` is ignored with a warning when PACER cannot serve the case. V4 can read stored docket material without an upfront premium gate; live PACER acquisition is metered, and an explicit refresh requires premium access. A first acquisition can require PACER even when `forceFetch` is omitted. The same [included usage and consent requirements](#docket-content-and-pacer-retrieval) apply with either setting. ```json { "docket": { "court": "N.D. Ill.", "docketNumber": "1:16-cv-11057" }, "question": "What was filed or entered during the past month?", "forceFetch": true } ``` ### Output | Field | Meaning | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `status` | `ok` on a successful analysis. | | `case` | `caseId`, `caseName`, `court`, reader `url`, and available docket/date metadata. | | `answer` | Answer based on available metadata and recorded entries. Metadata-only answers can have no selected entries. | | `relevantEntries` | Up to 40 entries with `hasDocument` and available `entryNumber`, `attachmentNumber`, `dateFiled`, `description`, and `documentReference`. Stored entries may have `entryId`. | | `doesNotAddress` | Unanswered parts of the question; empty when answered. | | `coverage` | `kind`: `docket_report`, `stored_entries`, or `opinion_only`; `entryCount`: analyzed entries; `truncated`: source or returned selection is incomplete. Read warnings and narrow the question when needed. | | `freshness` | `cached`, `liveFetchSucceeded`, `fetchedAt` (acquisition timestamp or `null`), and `latestEntryDate` (latest recorded entry date or `null`). | | `retrievalSource` | `midpage`, `recap`, or `pacer`. | | `docketReport` | Optional `downloadUrl` and `suggestedFileName` for the report file. | | `warnings` | Optional source and retrieval caveats. | `documentReference` is either `{ "documentId": "..." }` or `{ "entry": { "caseId": "...", "entryNumber": "...", "attachmentNumber": null } }`. Attachments use their number instead of `null`. Pass the object unchanged to `analyzeCaseDocument` and add `question`. A reference indicates an available retrieval path, not a guarantee that fetching will succeed. Use the two report links for different purposes: | Field | Purpose | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `case.url` | Citations and reader links such as **View docket**. Opens the docket reader. | | `docketReport.downloadUrl` | Fetching the original report for download or processing. Link it in an answer only when offering an explicit download. | Copy these URLs exactly. Do not construct raw-download paths from reader URLs or use a download URL for a “View docket” link. A recent acquisition timestamp does not prove the underlying report is current. In particular, a newly acquired RECAP report can be stale. A failed refresh does not establish that no new filings exist. Use `liveFetchSucceeded`, coverage, the requested period, and warnings when describing current status. Other statuses are `invalid_reference`, `not_found`, `ambiguous`, `pacer_fetch_failed`, and `analysis_failed`, each with a `message`. Ambiguous lookups can return `candidates`. An analysis failure can also return a top-level `downloadUrl` for manual reading. ## searchLaws Full-text search over statutes, regulations, constitutions, and agency guidance — US federal plus all 50 states and DC. Use it to FIND the governing provision, then call `analyzeLaw` with the result's `id` before citing it. Results are current law by default (see the [Laws API guide](/documentation/integration/laws-api) for the currency derivation). Inputs are strict: unknown keys are rejected rather than silently ignored. **Parameters:** | Parameter | Type | Required | Description | | ------------------ | --------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `query` | string | Yes | Legal concepts / key terms (BM25 keyword search) | | `states` | string\[] | No | State names or USPS abbreviations (`"California"`, `"TX"`); federal law is `"Federal"`. Validated server-side — unrecognized values error with did-you-mean suggestions | | `collectionTypes` | string\[] | No | `statute`, `regulation`, `constitution`, `guidance`, `executive_order`, `notice` | | `collections` | string\[] | No | Specific codes, by id (e.g. `"cfr"`, `"wa-rcw"`) or human-readable name (e.g. `"Code of Federal Regulations"`, `"Texas Administrative Code"`) — resolved server-side; unresolvable values error with did-you-mean suggestions | | `pathPrefix` | string | No | Subtree filter within one code — a dot-separated ltree path from a previous result (e.g. `"cfr.t21"` = all of 21 CFR, including descendants); pair with `collections`. Malformed prefixes error rather than matching nothing | | `currentOnly` | boolean | No | Default true — only the law in force today | | `page`, `pageSize` | number | No | Pagination (max 50 per page; results beyond the first 10,000 cannot be paginated) | **Returns:** | Field | Type | Description | | ------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `results` | array | Matching provisions: `id`, `citation`, `title`, `number`, jurisdiction and path fields, dates, `isCurrent` / `isHistorical`, `snippets` (matched fragments), and `url` (the Midpage page for that exact version) | | `total` | number | Estimate for the whole query; a page can hold fewer than `pageSize` results | | `stateCounts` | object | Estimated per-state hit counts for the WHOLE query — for state surveys, run one unfiltered query and read this, then verify per state with a filtered search | | `error` | string | Set instead of results when a filter could not be resolved or the pagination window was exceeded — the message says how to fix the call | Never cite from `snippets` — they are relevance context. Call `analyzeLaw` with the result `id` and quote from its verified passages. **Example:** ```json { "query": "telehealth prescribing controlled substances", "collectionTypes": ["regulation"], "states": ["California", "Texas"] } ``` ## analyzeLaw Analyze a statute, regulation, constitutional provision, or agency guidance document against a specific legal question, returning verified verbatim passages — the document text is analyzed internally and raw text is never returned. Resolves to the CURRENT version by default (see the [Laws API guide](/documentation/integration/laws-api) for how currency is derived); container nodes return their children for navigation; versioned provisions return the full version chain with dates. **Parameters** (provide exactly one of the four lookup keys): | Parameter | Type | Required | Description | | ------------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `citation` | string | No\* | Canonical citation (e.g., `"42 U.S.C. § 1983"`, `"Cal. Code Regs. tit. 22, § 51451"`). Matching ignores punctuation, spacing, and `§`/`sec.`/`section` style (`"42 USC 1983"` works), but not abbreviation differences — use the jurisdiction's canonical wording | | `registerCitation` | string | No\* | Federal Register / state register citation (e.g., `"89 FR 12345"`) | | `path` | string | No\* | Full ltree path from a previous result (pair with `collection`) | | `id` | string | No\* | Document UUID from a previous result | | `question` | string | Yes | The legal question to answer about the document | | `collection` | string | No | Disambiguates path/citation/registerCitation lookups — a collection id (e.g., `"cfr"`, `"uscode"`) or human-readable name (e.g., `"Code of Federal Regulations"`); unresolvable values error with did-you-mean suggestions | | `includeHistorical` | boolean | No | Allow superseded / future-effective versions in citation/path/registerCitation lookups (default: current law only); `id` always returns that exact version | \* Provide exactly one of `citation`, `registerCitation`, `path`, or `id` **Returns:** | Field | Type | Description | | ---------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `citation`, `title`, `number` | string | Identification of the provision | | `registerCitation` | string | FR / state-register cite, for regulations that carry one | | `collectionId`, `collectionName` | string | The code the provision belongs to (e.g. `cfr` / "Code of Federal Regulations") | | `effectiveDate`, `publicationDate` | string | Source-stated dates (null when the source states none) | | `analysis` | object | `summary`, `passages` (`point` + verified verbatim `quote` + `deeplinkUrl`, a Midpage link that opens the provision at the quoted passage), `doesNotAddress` — aspects of the question the document does not cover; check it before citing. Absent for containers without their own text | | `children` | array | For containers: up to 250 child provisions (`id`, `number`, `citation`, `title`, `sortOrder`) — navigation only, analyze a child by its `id`; `childrenTotal` is the true count | | `versions` | array | Version chain with dates and `isHistorical` / `isCurrent` / `isRequested` flags (empty without history) | | `parentId` | string | Container node id — look it up to zoom out | | `isCurrent`, `isHistorical` | boolean | Whether this text is the law in force today / whether it has been superseded | | `state`, `collectionType` | string | Jurisdiction (`"Federal"` or state name) and statute/regulation/constitution/guidance/executive\_order/notice | | `url`, `sourceUrl` | string | Midpage link for this exact version (non-current versions carry `?version=`) and official source link | | `ambiguousMatches` | array | When the reference matched several documents — candidate summaries; retry with one match's `id` | | `error` | string | The reference could not be resolved or the provision could not be analyzed — the message says how to proceed (some provisions are review-only via `url`) | **Example:** ```json { "citation": "21 C.F.R. § 820.30", "question": "What design control requirements apply to medical device manufacturers?" } ``` ## Migrate from v3 | V3 | V4 | | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | `search` with opinion-focused filters and per-query results | `search` with case/document identities, exact document types, one merged ranking, one status, and one cursor. | | `analyzeOpinion` and `findInOpinion` | `analyzeCaseDocument` for question-driven opinion or filing analysis. No standalone `findInOpinion` tool. | | `analyzeDocketReport` | `analyzeCaseDocket` for report metadata and recorded activity. | | `analyzeDocketFiling` | `analyzeCaseDocument` with a document UUID or entry reference. | | `searchLaws`, `analyzeLaw` | Same contracts. | V3 docket tools keep their premium-access gate. V4 admits stored material without that upfront gate; actual PACER retrieval remains metered. V4 is not a drop-in replacement: update tool names, reference fields, response parsing, citation links, and pagination together. Keep integrations pinned to their existing version until they are ready to migrate. ## Changelog See the [MCP changelog](/documentation/integration/mcp-tools/changelog) for version history and migration notes. # MCP v3 Integrate Midpage's legal database with AI assistants using the Model Context Protocol. ## Versioning These docs describe MCP `v3`. The current contract is [MCP v4](/documentation/integration/mcp-tools), which replaces the opinion and docket tools with `search`, `analyzeCaseDocument`, and `analyzeCaseDocket`; see its [migration notes](/documentation/integration/mcp-tools#migrate-from-v3). Midpage uses major versions for tool contract changes that integrations may need to account for, such as output schema changes or newly introduced tools. If your integration depends on fixed tool schemas or output fields, use a pinned version endpoint. Other version docs and migration notes: * [MCP v4](/documentation/integration/mcp-tools): `https://app.midpage.ai/mcp/v4` * [MCP v2](/documentation/integration/mcp-tools/v2): `https://app.midpage.ai/mcp/v2` * [MCP v1](/documentation/integration/mcp-tools/v1): `https://app.midpage.ai/mcp/v1` * [MCP changelog](/documentation/integration/mcp-tools/changelog) ## Base URL ```text https://app.midpage.ai/mcp/v3 ``` Use this pinned URL for the `v3` contract documented here. The unversioned URL serves the current contract, `v4`, not this one: ```text https://app.midpage.ai/mcp ``` ## Authentication Midpage supports two authentication models for MCP integrations. | Option | Best for | How it works | | ------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | API key | Server-side integrations, internal tools, and quick prototypes | Your integration sends one shared API key with each request. | | OAuth | End-user apps, partner integrations, and multi-tenant products | Each user signs in with their own Midpage account and your client sends that user's access token. | ### 1. API Key Use API key auth for non-interactive access, server-side jobs, or any integration that should run under one shared credential. Generate an API key in the [Developer Portal](https://app.midpage.ai/developers). If you need help, contact . Then send it in your MCP request headers as: ```http Authorization: Bearer ``` ### 2. OAuth Use OAuth when your client should connect each user to their own Midpage account. Each user will need a Midpage account before they can sign in and authorize your client. For most customers, the easiest setup is: 1. Point your client at `https://app.midpage.ai/mcp/v3` 2. Let your MCP or OAuth library follow Midpage's discovery metadata and register a client automatically if it supports dynamic client registration 3. Let the user sign in and approve access when prompted Most MCP clients should not need pre-provisioned OAuth app credentials. Midpage's Clerk auth server supports dynamic client registration, so compatible clients can create their own credentials automatically. If your client cannot do that and needs a pre-provisioned OAuth app, contact Midpage. Need the manual OAuth settings? If your client supports discovery, start there. In most cases, pointing it at `https://app.midpage.ai/mcp/v3` is enough. Midpage publishes protected-resource metadata on `app.midpage.ai`, and that metadata points clients to the Clerk authorization server. The settings below are only for cases where you need to configure the OAuth flow yourself. **Discovery** * MCP protected-resource metadata: `https://app.midpage.ai/.well-known/oauth-protected-resource/mcp` * Clerk OAuth metadata: `https://clerk.midpage.ai/.well-known/oauth-authorization-server` * Clerk OIDC discovery: `https://clerk.midpage.ai/.well-known/openid-configuration` Some older MCP clients expect OAuth authorization-server metadata on the same origin as the MCP server. Midpage currently publishes protected-resource metadata on `app.midpage.ai` and authorization-server metadata on the Clerk domain above, so older clients may need manual configuration using those Clerk URLs. The current metadata advertises: * authorization server: `https://clerk.midpage.ai` * dynamic client registration endpoint: `https://clerk.midpage.ai/oauth/register` * PKCE challenge method: `S256` * resource scopes: `profile`, `email` **Which OAuth flow to use** * Public clients such as desktop apps, mobile apps, browser apps, and local CLIs should use Authorization Code + PKCE (`S256`). Do not embed a `client_secret` in those clients. If you need this mode, Midpage can provision your Clerk OAuth app as a public client. * Confidential server-side apps can use Authorization Code and exchange the code on their backend with either `client_secret_basic` or `client_secret_post`. * If you need refresh tokens for long-lived sessions, request `offline_access`. **Minimal flow** 1. Fetch the protected-resource metadata document and read `authorization_servers`. 2. Fetch the Clerk authorization-server or OIDC metadata document and use the published endpoints. 3. Start an authorization request with `response_type=code`, `client_id`, `redirect_uri`, `state`, and the `resource` value from the protected-resource metadata document. 4. Request `profile email` for MCP access. Add `openid` if your client expects an ID token. Add `offline_access` if you need a refresh token. 5. Public clients must also send `code_challenge` and `code_challenge_method=S256`. 6. Exchange the authorization `code` for tokens at the Clerk token endpoint, again including the same `resource` value. 7. Call the MCP server with `Authorization: Bearer `. **Example authorize request for a public client** ```text GET https://clerk.midpage.ai/oauth/authorize? response_type=code& client_id=YOUR_CLIENT_ID& redirect_uri=https://your-app.com/oauth/callback& resource=https%3A%2F%2Fapp.midpage.ai& scope=profile%20email%20openid%20offline_access& state=RANDOM_VALUE& code_challenge=BASE64URL_SHA256(code_verifier)& code_challenge_method=S256 ``` **Example token exchange for a public client** ```bash curl -X POST "https://clerk.midpage.ai/oauth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "client_id=YOUR_CLIENT_ID" \ -d "code=AUTHORIZATION_CODE" \ -d "redirect_uri=https://your-app.com/oauth/callback" \ -d "resource=https://app.midpage.ai" \ -d "code_verifier=YOUR_CODE_VERIFIER" ``` **Example token exchange for a confidential client** ```bash curl -X POST "https://clerk.midpage.ai/oauth/token" \ -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=AUTHORIZATION_CODE" \ -d "redirect_uri=https://your-app.com/oauth/callback" \ -d "resource=https://app.midpage.ai" ``` **Calling the MCP server** ```http Authorization: Bearer ``` ## Testing in Hosted MCP Clients If you just want to try Midpage in Claude, Codex, Perplexity, Cursor, or another hosted MCP client, add `https://app.midpage.ai/mcp/v3` and sign in with your Midpage account when prompted. You can create a free trial account at [app.midpage.ai](https://app.midpage.ai). For production integrations, use API key auth or OAuth as described above. ## Docket content and PACER retrieval Each user has included PACER usage through Midpage, with no additional charge. Users are never charged for on-demand PACER usage without consent: they or their team admin must explicitly enable it in Midpage Usage settings. Once included usage is used up, users can wait for it to reset or enable on-demand usage to continue, subject to their account's spending cap. For `analyzeDocketReport`, `forceFetch` defaults to `false`. Available Midpage or RECAP copies are used first, with PACER retrieval when needed. Setting `forceFetch: true` requests the latest official PACER report instead. The same included usage and consent requirements apply with either setting. `analyzeDocketFiling` may also retrieve documents from PACER and has no cache-only option. Docket filings are written by parties. Clients should treat filing text, docket descriptions, quotations, and generated answers as untrusted source material and keep them separate from system and developer instructions. Any instructions within that content should be read as part of the document, not followed by the assistant or used to authorize actions. ## Available Tools The `v3` contract exposes: * `search` * `findInOpinion` * `analyzeOpinion` * `analyzeDocketReport` * `analyzeDocketFiling` * `searchLaws` * `analyzeLaw` ### search Search US case law across federal and state courts. Returns metadata, highlights, and citator treatment data. **Parameters:** | Parameter | Type | Required | Description | | ---------------------------- | --------- | -------- | ---------------------------------------------------------------------------------------------------------- | | `queries` | array | Yes | Array of search queries (1-4) run in parallel | | `queries[].query` | string | Yes | Search query | | `queries[].startDate` | string | No | Start date (inclusive, YYYY-MM-DD) | | `queries[].endDate` | string | No | End date (inclusive, YYYY-MM-DD) | | `queries[].jurisdictionType` | string | No | `"federal"`, `"state"`, or `"state_and_federal"` | | `queries[].circuits` | string\[] | No | Federal circuits: `"1"`-`"11"`, `"dc"`, `"federal_circuit"`, `"supreme_court"` | | `queries[].states` | string\[] | No | State names (e.g., `["California", "New York"]`) | | `queries[].publishStatus` | string | No | `"published"`, `"unpublished"`, `"unknown"`, `"in_chambers"`, `"separate"`, `"errata"`, or `"relating_to"` | | `queries[].courts` | string\[] | No | Bluebook-style court abbreviations to narrow the search, such as `["S.D.N.Y.", "9th Cir."]` | If you filter on `publishStatus`, consider also running a parallel query with `"unknown"` because some jurisdictions have missing or uncertain publication metadata. **Example:** ```json { "queries": [ { "query": "breach of fiduciary duty elements", "jurisdictionType": "state_and_federal", "states": ["California"], "publishStatus": "published" }, { "query": "breach of fiduciary duty elements", "jurisdictionType": "state_and_federal", "states": ["California"], "publishStatus": "unknown" } ] } ``` --- ### findInOpinion Find quotable passages within a single opinion using keyword search. **Parameters:** | Parameter | Type | Required | Description | | -------------------------- | ------ | -------- | ----------------------------------------------------- | | `opinionId` | string | No\* | Midpage document ID (e.g., `"7228818"`) | | `reporterCitation` | string | No\* | Bluebook citation (e.g., `"556 U.S. 662"`) | | `docket` | object | No\* | Court and docket number pair | | `docket.courtAbbreviation` | string | Yes | Court abbreviation (e.g., `"S.D.N.Y."`, `"9th Cir."`) | | `docket.docketNumber` | string | Yes | Docket number (e.g., `"12-cv-20100"`) | | `query` | string | Yes | Key terms to match | \* Provide one of `opinionId`, `reporterCitation`, or `docket` **Example:** ```json { "opinionId": "7228818", "query": "minimum contacts purposeful availment" } ``` --- ### analyzeOpinion Reads the full document, then answers your question and returns the answer alongside text excerpts. Accepts either `opinionId`, `reporterCitation`, or `docket` as input. **Parameters:** | Parameter | Type | Required | Description | | -------------------------- | ------ | -------- | ----------------------------------------------------- | | `opinionId` | string | No\* | Midpage document ID | | `reporterCitation` | string | No\* | Bluebook citation (e.g., `"556 U.S. 662"`) | | `docket` | object | No\* | Court and docket number pair | | `docket.courtAbbreviation` | string | Yes | Court abbreviation (e.g., `"S.D.N.Y."`, `"9th Cir."`) | | `docket.docketNumber` | string | Yes | Docket number (e.g., `"12-cv-20100"`) | | `question` | string | Yes | The legal question you want answered | \* Provide one of `opinionId`, `reporterCitation`, or `docket` **Example:** ```json { "reporterCitation": "556 U.S. 662", "question": "What standard does this case establish for pleading requirements?" } ``` **Output fields:** | Field | Type | Description | | ---------------------------------------- | --------- | --------------------------------------------------------------------------------- | | `opinionId` | string | Midpage opinion ID | | `question` | string | The question that was analyzed | | `citation` | string | Bluebook citation for the opinion | | `url` | string | General Midpage document URL | | `supportedPropositions` | array | Propositions the opinion actually supports | | `supportedPropositions[].proposition` | string | Cite-ready proposition supported by the quote | | `supportedPropositions[].quote` | string | Verbatim supporting text from the opinion | | `supportedPropositions[].scope` | string | Limits, conditions, or context for the proposition | | `supportedPropositions[].centrality` | string | `core_holding`, `supporting_analysis`, `secondary_matter`, or `background` | | `supportedPropositions[].opinionSection` | string | Optional source label for non-majority opinions, such as a concurrence or dissent | | `supportedPropositions[].deeplinkURL` | string | Midpage URL linked to the supporting line range | | `doesNotAddress` | string\[] | Topics from the question the opinion does not address | | `summary` | string | Short summary of what the case is about | | `disposition` | string | Short procedural outcome phrase | | `jurisdiction` | string | Court and jurisdiction | | `treatment` | object | Citator treatment summary | **Example output:** ```json { "opinionId": "7228818", "question": "What does this case hold?", "citation": "Example v. Case, 1 F.4th 1 (9th Cir. 2024)", "url": "https://app.midpage.ai/document/7228818", "supportedPropositions": [ { "proposition": "The court adopted the rule.", "quote": "We adopt the rule.", "scope": "Applies in the stated procedural posture.", "centrality": "core_holding", "opinionSection": "Smith, Concurring", "deeplinkURL": "https://app.midpage.ai/document/7228818?lines=10-12" } ], "doesNotAddress": [], "summary": "The court adopted a rule.", "disposition": "affirming judgment", "jurisdiction": "9th Circuit", "treatment": { "status": "positive", "citedBy": 10 } } ``` --- ### analyzeDocketReport Analyze a federal docket report and answer a docket-level question. Use this for case snapshots, recent activity, procedural posture, service, complaint history, motion history, deadlines, and identifying filings that should be reviewed next. This tool answers from the docket sheet. To read a specific filing, pass the returned `case.caseId`, `relevantEntries[].entryNumber`, and optional `relevantEntries[].attachmentNumber` to `analyzeDocketFiling`. See [Docket content and PACER retrieval](#docket-content-and-pacer-retrieval) for source handling, included usage, and on-demand billing. **Parameters:** | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `court` | string | No\* | Federal court. Prefer Bluebook-style abbreviations such as `"N.D. Cal."`, `"S.D.N.Y."`, `"D. Del."`, `"Bankr. D. Del."`, `"9th Cir."`, or `"Fed. Cir."`. Full federal court names are also accepted. | | `docketNumber` | string | No\* | Federal case docket number. Prefer full district or bankruptcy numbers with office or division prefix when known, such as `"4:24-cv-04722"`; appellate numbers look like `"24-1234"`. | | `caseName` | string | No | Optional case caption/name, such as `"Musk v. Altman"`. Include it when available, especially if the docket number is incomplete. | | `forceFetch` | boolean | No | Default `false`. Set `true` to skip available copies and request the latest official PACER report. Both settings use included usage first; on-demand charges require opt-in. | | `question` | string | Yes | The docket-level question to answer. | \* `court` and `docketNumber` are optional in the input schema but required to retrieve a report. Case-name-only lookup is not supported. **Example:** ```json { "court": "N.D. Cal.", "docketNumber": "4:24-cv-04722", "caseName": "Musk v. Altman", "forceFetch": false, "question": "Give me a case snapshot and identify the operative complaint." } ``` **Output fields:** | Field | Type | Description | | ------------------------------------ | -------------- | --------------------------------------------------------------------------- | | `status` | string | `"ok"` or a specific resolution/analysis error | | `case` | object | Resolved case metadata | | `case.caseId` | string | Case ID to use with `analyzeDocketFiling` | | `case.caseName` | string or null | Resolved case caption, when available | | `case.court` | string | Resolved court | | `case.docketNumber` | string | Resolved docket number | | `docketReport` | object | Docket report file metadata | | `docketReport.url` | string or null | Downloadable Midpage file URL for the docket report, when available | | `docketReport.suggestedFileName` | string | Suggested filename if saving the docket report | | `answer` | string | Direct answer based only on the docket sheet | | `relevantEntries` | array | Docket entries or attachments supporting the answer or worth reviewing next | | `relevantEntries[].entryNumber` | string | Docket entry number to pass to `analyzeDocketFiling` | | `relevantEntries[].attachmentNumber` | string or null | Optional attachment number to pass to `analyzeDocketFiling` | | `relevantEntries[].dateFiled` | string or null | Optional filing date | | `relevantEntries[].description` | string | Docket entry description, when available | | `relevantEntries[].relevance` | string | Why the entry matters | | `doesNotAddress` | string\[] | Parts of the question the docket report does not answer | | `warnings` | string\[] | Optional warnings about report coverage or parsing | --- ### analyzeDocketFiling Analyze one docket filing selected from `analyzeDocketReport` and answer a focused question using passages from that filing. Use this when the user needs to understand what a specific filing says, not just what the docket entry says. See [Docket content and PACER retrieval](#docket-content-and-pacer-retrieval) for source handling, included usage, and on-demand billing. This tool has no `forceFetch` or cache-only parameter. **Parameters:** | Parameter | Type | Required | Description | | ------------------ | -------------- | -------- | --------------------------------------------------------------------------------------------------------------- | | `caseId` | string | Yes | `case.caseId` returned by `analyzeDocketReport` | | `entryNumber` | string | Yes | `relevantEntries[].entryNumber` returned by `analyzeDocketReport` | | `attachmentNumber` | string or null | No | `relevantEntries[].attachmentNumber` returned by `analyzeDocketReport`. Omit or set `null` for the main filing. | | `question` | string | Yes | The question to answer about this specific filing | `caseId`, `entryNumber`, and `question` must be nonempty strings. `filingId` is returned as metadata in the response; it is not an input parameter. **Example:** ```json { "caseId": "CASE_ID_FROM_DOCKET_REPORT", "entryNumber": "ENTRY_NUMBER_FROM_DOCKET_REPORT", "attachmentNumber": null, "question": "What claims are asserted in this complaint?" } ``` Replace the placeholders with values returned by `analyzeDocketReport`. For an attachment, also supply its returned `attachmentNumber`. **Output fields:** | Field | Type | Description | | -------------------------------- | -------------- | ----------------------------------------------------- | | `status` | string | `"ok"` or a specific resolution/analysis error | | `filing` | object | Resolved filing metadata | | `filing.caseId` | string | Case ID | | `filing.filingId` | string | Filing ID | | `filing.entryNumber` | string | Docket entry number | | `filing.attachmentNumber` | string or null | Optional attachment number | | `filing.dateFiled` | string or null | Filing date, when available | | `filing.description` | string | Docket entry description for the filing | | `filing.url` | string | Downloadable Midpage file URL for the filing | | `filing.suggestedFileName` | string | Suggested filename if saving the filing | | `answer` | string | Direct answer based only on the filing text | | `supportingPassages` | array | Passages from the filing supporting the answer | | `supportingPassages[].passage` | string | Supporting passage text | | `supportingPassages[].relevance` | string | Why the passage matters | | `doesNotAddress` | string\[] | Parts of the question the filing text does not answer | When citing filing content in a user-facing answer, hyperlink the citation to `filing.url`. Include the ECF number in the citation when available, for example: `[Pl.'s Mot. for Summ. J. 5, ECF No. 30 (Mar. 3, 2020)]()`. --- ### searchLaws Full-text search over statutes, regulations, constitutions, and agency guidance — US federal plus all 50 states and DC. Use it to FIND the governing provision, then call `analyzeLaw` with the result's `id` before citing it. Results are current law by default (see the [Laws API guide](/documentation/integration/laws-api) for the currency derivation). Inputs are strict: unknown keys are rejected rather than silently ignored. **Parameters:** | Parameter | Type | Required | Description | | ------------------ | --------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `query` | string | Yes | Legal concepts / key terms (BM25 keyword search) | | `states` | string\[] | No | State names or USPS abbreviations (`"California"`, `"TX"`); federal law is `"Federal"`. Validated server-side — unrecognized values error with did-you-mean suggestions | | `collectionTypes` | string\[] | No | `statute`, `regulation`, `constitution`, `guidance`, `executive_order`, `notice` | | `collections` | string\[] | No | Specific codes, by id (e.g. `"cfr"`, `"wa-rcw"`) or human-readable name (e.g. `"Code of Federal Regulations"`, `"Texas Administrative Code"`) — resolved server-side; unresolvable values error with did-you-mean suggestions | | `pathPrefix` | string | No | Subtree filter within one code — a dot-separated ltree path from a previous result (e.g. `"cfr.t21"` = all of 21 CFR, including descendants); pair with `collections`. Malformed prefixes error rather than matching nothing | | `currentOnly` | boolean | No | Default true — only the law in force today | | `page`, `pageSize` | number | No | Pagination (max 50 per page; results beyond the first 10,000 cannot be paginated) | **Returns:** | Field | Type | Description | | ------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `results` | array | Matching provisions: `id`, `citation`, `title`, `number`, jurisdiction and path fields, dates, `isCurrent` / `isHistorical`, `snippets` (matched fragments), and `url` (the Midpage page for that exact version) | | `total` | number | Estimate for the whole query; a page can hold fewer than `pageSize` results | | `stateCounts` | object | Estimated per-state hit counts for the WHOLE query — for state surveys, run one unfiltered query and read this, then verify per state with a filtered search | | `error` | string | Set instead of results when a filter could not be resolved or the pagination window was exceeded — the message says how to fix the call | Never cite from `snippets` — they are relevance context. Call `analyzeLaw` with the result `id` and quote from its verified passages. **Example:** ```json { "query": "telehealth prescribing controlled substances", "collectionTypes": ["regulation"], "states": ["California", "Texas"] } ``` --- ### analyzeLaw Analyze a statute, regulation, constitutional provision, or agency guidance document against a specific legal question, returning verified verbatim passages — the document text is analyzed internally and raw text is never returned. Resolves to the CURRENT version by default (see the [Laws API guide](/documentation/integration/laws-api) for how currency is derived); container nodes return their children for navigation; versioned provisions return the full version chain with dates. **Parameters** (provide exactly one of the four lookup keys): | Parameter | Type | Required | Description | | ------------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `citation` | string | No\* | Canonical citation (e.g., `"42 U.S.C. § 1983"`, `"Cal. Code Regs. tit. 22, § 51451"`). Matching ignores punctuation, spacing, and `§`/`sec.`/`section` style (`"42 USC 1983"` works), but not abbreviation differences — use the jurisdiction's canonical wording | | `registerCitation` | string | No\* | Federal Register / state register citation (e.g., `"89 FR 12345"`) | | `path` | string | No\* | Full ltree path from a previous result (pair with `collection`) | | `id` | string | No\* | Document UUID from a previous result | | `question` | string | Yes | The legal question to answer about the document | | `collection` | string | No | Disambiguates path/citation/registerCitation lookups — a collection id (e.g., `"cfr"`, `"uscode"`) or human-readable name (e.g., `"Code of Federal Regulations"`); unresolvable values error with did-you-mean suggestions | | `includeHistorical` | boolean | No | Allow superseded / future-effective versions in citation/path/registerCitation lookups (default: current law only); `id` always returns that exact version | \* Provide exactly one of `citation`, `registerCitation`, `path`, or `id` **Returns:** | Field | Type | Description | | ---------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `citation`, `title`, `number` | string | Identification of the provision | | `registerCitation` | string | FR / state-register cite, for regulations that carry one | | `collectionId`, `collectionName` | string | The code the provision belongs to (e.g. `cfr` / "Code of Federal Regulations") | | `effectiveDate`, `publicationDate` | string | Source-stated dates (null when the source states none) | | `analysis` | object | `summary`, `passages` (`point` + verified verbatim `quote` + `deeplinkUrl`, a Midpage link that opens the provision at the quoted passage), `doesNotAddress` — aspects of the question the document does not cover; check it before citing. Absent for containers without their own text | | `children` | array | For containers: up to 250 child provisions (`id`, `number`, `citation`, `title`, `sortOrder`) — navigation only, analyze a child by its `id`; `childrenTotal` is the true count | | `versions` | array | Version chain with dates and `isHistorical` / `isCurrent` / `isRequested` flags (empty without history) | | `parentId` | string | Container node id — look it up to zoom out | | `isCurrent`, `isHistorical` | boolean | Whether this text is the law in force today / whether it has been superseded | | `state`, `collectionType` | string | Jurisdiction (`"Federal"` or state name) and statute/regulation/constitution/guidance/executive\_order/notice | | `url`, `sourceUrl` | string | Midpage link for this exact version (non-current versions carry `?version=`) and official source link | | `ambiguousMatches` | array | When the reference matched several documents — candidate summaries; retry with one match's `id` | | `error` | string | The reference could not be resolved or the provision could not be analyzed — the message says how to proceed (some provisions are review-only via `url`) | **Example:** ```json { "citation": "21 C.F.R. § 820.30", "question": "What design control requirements apply to medical device manufacturers?" } ``` ## Changelog See the [MCP changelog](/documentation/integration/mcp-tools/changelog) for version history and migration notes. # MCP v2 Integrate Midpage's legal database with AI assistants using the Model Context Protocol. > **Note:** MCP v2 covers court opinion research only. Statute and regulation tools (`searchLaws`, `analyzeLaw`) are available on [MCP v3](/documentation/integration/mcp-tools/v3). ## Versioning These docs describe the current MCP contract, `v2`. Midpage uses major versions for tool contract changes that integrations may need to account for, such as output schema changes or newly introduced tools. If your integration depends on fixed tool schemas or output fields, use a pinned version endpoint. Previous version docs and migration notes: * [MCP v1](/documentation/integration/mcp-tools/v1): `https://app.midpage.ai/mcp/v1` * [MCP changelog](/documentation/integration/mcp-tools/changelog) ## Base URL ```text https://app.midpage.ai/mcp/v2 ``` Use this pinned URL for the `v2` contract documented here. If you want your integration to automatically receive the latest tool implementations, use the unversioned URL instead: ```text https://app.midpage.ai/mcp ``` ## Authentication Midpage supports two authentication models for MCP integrations. | Option | Best for | How it works | | ------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | API key | Server-side integrations, internal tools, and quick prototypes | Your integration sends one shared API key with each request. | | OAuth | End-user apps, partner integrations, and multi-tenant products | Each user signs in with their own Midpage account and your client sends that user's access token. | ### 1. API Key Use API key auth for non-interactive access, server-side jobs, or any integration that should run under one shared credential. Generate an API key in the [Developer Portal](https://app.midpage.ai/developers). If you need help, contact . Then send it in your MCP request headers as: ```http Authorization: Bearer ``` ### 2. OAuth Use OAuth when your client should connect each user to their own Midpage account. Each user will need a Midpage account before they can sign in and authorize your client. For most customers, the easiest setup is: 1. Point your client at `https://app.midpage.ai/mcp/v2` 2. Let your MCP or OAuth library follow Midpage's discovery metadata and register a client automatically if it supports dynamic client registration 3. Let the user sign in and approve access when prompted Most MCP clients should not need pre-provisioned OAuth app credentials. Midpage's Clerk auth server supports dynamic client registration, so compatible clients can create their own credentials automatically. If your client cannot do that and needs a pre-provisioned OAuth app, contact Midpage. Need the manual OAuth settings? If your client supports discovery, start there. In most cases, pointing it at `https://app.midpage.ai/mcp/v2` is enough. Midpage publishes protected-resource metadata on `app.midpage.ai`, and that metadata points clients to the Clerk authorization server. The settings below are only for cases where you need to configure the OAuth flow yourself. **Discovery** * MCP protected-resource metadata: `https://app.midpage.ai/.well-known/oauth-protected-resource/mcp` * Clerk OAuth metadata: `https://clerk.midpage.ai/.well-known/oauth-authorization-server` * Clerk OIDC discovery: `https://clerk.midpage.ai/.well-known/openid-configuration` Some older MCP clients expect OAuth authorization-server metadata on the same origin as the MCP server. Midpage currently publishes protected-resource metadata on `app.midpage.ai` and authorization-server metadata on the Clerk domain above, so older clients may need manual configuration using those Clerk URLs. The current metadata advertises: * authorization server: `https://clerk.midpage.ai` * dynamic client registration endpoint: `https://clerk.midpage.ai/oauth/register` * PKCE challenge method: `S256` * resource scopes: `profile`, `email` **Which OAuth flow to use** * Public clients such as desktop apps, mobile apps, browser apps, and local CLIs should use Authorization Code + PKCE (`S256`). Do not embed a `client_secret` in those clients. If you need this mode, Midpage can provision your Clerk OAuth app as a public client. * Confidential server-side apps can use Authorization Code and exchange the code on their backend with either `client_secret_basic` or `client_secret_post`. * If you need refresh tokens for long-lived sessions, request `offline_access`. **Minimal flow** 1. Fetch the protected-resource metadata document and read `authorization_servers`. 2. Fetch the Clerk authorization-server or OIDC metadata document and use the published endpoints. 3. Start an authorization request with `response_type=code`, `client_id`, `redirect_uri`, `state`, and the `resource` value from the protected-resource metadata document. 4. Request `profile email` for MCP access. Add `openid` if your client expects an ID token. Add `offline_access` if you need a refresh token. 5. Public clients must also send `code_challenge` and `code_challenge_method=S256`. 6. Exchange the authorization `code` for tokens at the Clerk token endpoint, again including the same `resource` value. 7. Call the MCP server with `Authorization: Bearer `. **Example authorize request for a public client** ```text GET https://clerk.midpage.ai/oauth/authorize? response_type=code& client_id=YOUR_CLIENT_ID& redirect_uri=https://your-app.com/oauth/callback& resource=https%3A%2F%2Fapp.midpage.ai& scope=profile%20email%20openid%20offline_access& state=RANDOM_VALUE& code_challenge=BASE64URL_SHA256(code_verifier)& code_challenge_method=S256 ``` **Example token exchange for a public client** ```bash curl -X POST "https://clerk.midpage.ai/oauth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "client_id=YOUR_CLIENT_ID" \ -d "code=AUTHORIZATION_CODE" \ -d "redirect_uri=https://your-app.com/oauth/callback" \ -d "resource=https://app.midpage.ai" \ -d "code_verifier=YOUR_CODE_VERIFIER" ``` **Example token exchange for a confidential client** ```bash curl -X POST "https://clerk.midpage.ai/oauth/token" \ -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=AUTHORIZATION_CODE" \ -d "redirect_uri=https://your-app.com/oauth/callback" \ -d "resource=https://app.midpage.ai" ``` **Calling the MCP server** ```http Authorization: Bearer ``` ## Testing in Hosted MCP Clients If you just want to try Midpage in Claude, Codex, Perplexity, Cursor, or another hosted MCP client, add `https://app.midpage.ai/mcp/v2` and sign in with your Midpage account when prompted. You can create a free trial account at [app.midpage.ai](https://app.midpage.ai). For production integrations, use API key auth or OAuth as described above. ## Available Tools The latest MCP contract exposes: * `search` * `findInOpinion` * `analyzeOpinion` ### search Search US case law across federal and state courts. Returns metadata, highlights, and citator treatment data. **Parameters:** | Parameter | Type | Required | Description | | ---------------------------- | --------- | -------- | ---------------------------------------------------------------------------------------------------------- | | `queries` | array | Yes | Array of search queries (1-4) run in parallel | | `queries[].query` | string | Yes | Search query | | `queries[].startDate` | string | No | Start date (inclusive, YYYY-MM-DD) | | `queries[].endDate` | string | No | End date (inclusive, YYYY-MM-DD) | | `queries[].jurisdictionType` | string | No | `"federal"`, `"state"`, or `"state_and_federal"` | | `queries[].circuits` | string\[] | No | Federal circuits: `"1"`-`"11"`, `"dc"`, `"federal_circuit"`, `"supreme_court"` | | `queries[].states` | string\[] | No | State names (e.g., `["California", "New York"]`) | | `queries[].publishStatus` | string | No | `"published"`, `"unpublished"`, `"unknown"`, `"in_chambers"`, `"separate"`, `"errata"`, or `"relating_to"` | | `queries[].courts` | string\[] | No | Bluebook-style court abbreviations to narrow the search, such as `["S.D.N.Y.", "9th Cir."]` | If you filter on `publishStatus`, consider also running a parallel query with `"unknown"` because some jurisdictions have missing or uncertain publication metadata. **Example:** ```json { "queries": [ { "query": "breach of fiduciary duty elements", "jurisdictionType": "state_and_federal", "states": ["California"], "publishStatus": "published" }, { "query": "breach of fiduciary duty elements", "jurisdictionType": "state_and_federal", "states": ["California"], "publishStatus": "unknown" } ] } ``` --- ### findInOpinion Find quotable passages within a single opinion using keyword search. **Parameters:** | Parameter | Type | Required | Description | | -------------------------- | ------ | -------- | ----------------------------------------------------- | | `opinionId` | string | No\* | Midpage document ID (e.g., `"7228818"`) | | `reporterCitation` | string | No\* | Bluebook citation (e.g., `"556 U.S. 662"`) | | `docket` | object | No\* | Court and docket number pair | | `docket.courtAbbreviation` | string | Yes | Court abbreviation (e.g., `"S.D.N.Y."`, `"9th Cir."`) | | `docket.docketNumber` | string | Yes | Docket number (e.g., `"12-cv-20100"`) | | `query` | string | Yes | Key terms to match | \* Provide one of `opinionId`, `reporterCitation`, or `docket` **Example:** ```json { "opinionId": "7228818", "query": "minimum contacts purposeful availment" } ``` --- ### analyzeOpinion Reads the full document, then answers your question and returns the answer alongside text excerpts. Accepts either `opinionId`, `reporterCitation`, or `docket` as input. **Parameters:** | Parameter | Type | Required | Description | | -------------------------- | ------ | -------- | ----------------------------------------------------- | | `opinionId` | string | No\* | Midpage document ID | | `reporterCitation` | string | No\* | Bluebook citation (e.g., `"556 U.S. 662"`) | | `docket` | object | No\* | Court and docket number pair | | `docket.courtAbbreviation` | string | Yes | Court abbreviation (e.g., `"S.D.N.Y."`, `"9th Cir."`) | | `docket.docketNumber` | string | Yes | Docket number (e.g., `"12-cv-20100"`) | | `question` | string | Yes | The legal question you want answered | \* Provide one of `opinionId`, `reporterCitation`, or `docket` **Example:** ```json { "reporterCitation": "556 U.S. 662", "question": "What standard does this case establish for pleading requirements?" } ``` **Output fields:** | Field | Type | Description | | ---------------------------------------- | --------- | --------------------------------------------------------------------------------- | | `opinionId` | string | Midpage opinion ID | | `question` | string | The question that was analyzed | | `citation` | string | Bluebook citation for the opinion | | `url` | string | General Midpage document URL | | `supportedPropositions` | array | Propositions the opinion actually supports | | `supportedPropositions[].proposition` | string | Cite-ready proposition supported by the quote | | `supportedPropositions[].quote` | string | Verbatim supporting text from the opinion | | `supportedPropositions[].scope` | string | Limits, conditions, or context for the proposition | | `supportedPropositions[].centrality` | string | `core_holding`, `supporting_analysis`, `secondary_matter`, or `background` | | `supportedPropositions[].opinionSection` | string | Optional source label for non-majority opinions, such as a concurrence or dissent | | `supportedPropositions[].deeplinkURL` | string | Midpage URL linked to the supporting line range | | `doesNotAddress` | string\[] | Topics from the question the opinion does not address | | `summary` | string | Short summary of what the case is about | | `disposition` | string | Short procedural outcome phrase | | `jurisdiction` | string | Court and jurisdiction | | `treatment` | object | Citator treatment summary | **Example output:** ```json { "opinionId": "7228818", "question": "What does this case hold?", "citation": "Example v. Case, 1 F.4th 1 (9th Cir. 2024)", "url": "https://app.midpage.ai/document/7228818", "supportedPropositions": [ { "proposition": "The court adopted the rule.", "quote": "We adopt the rule.", "scope": "Applies in the stated procedural posture.", "centrality": "core_holding", "opinionSection": "Smith, Concurring", "deeplinkURL": "https://app.midpage.ai/document/7228818?lines=10-12" } ], "doesNotAddress": [], "summary": "The court adopted a rule.", "disposition": "affirming judgment", "jurisdiction": "9th Circuit", "treatment": { "status": "positive", "citedBy": 10 } } ``` ## Changelog See the [MCP changelog](/documentation/integration/mcp-tools/changelog) for version history and migration notes. # MCP v1 Integrate Midpage's legal database with AI assistants using the Model Context Protocol. > **Note:** MCP v1 covers court opinion research only. Statute and regulation tools (`searchLaws`, `analyzeLaw`) are available on [MCP v3](/documentation/integration/mcp-tools/v3). ## Base URL ```text https://app.midpage.ai/mcp/v1 ``` ## Authentication Midpage supports two authentication models for MCP integrations. | Option | Best for | How it works | | ------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | API key | Server-side integrations, internal tools, and quick prototypes | Your integration sends one shared API key with each request. | | OAuth | End-user apps, partner integrations, and multi-tenant products | Each user signs in with their own Midpage account and your client sends that user's access token. | ### 1. API Key Use API key auth for non-interactive access, server-side jobs, or any integration that should run under one shared credential. Generate an API key in the [Developer Portal](https://app.midpage.ai/developers). If you need help, contact . Then send it in your MCP request headers as: ```http Authorization: Bearer ``` ### 2. OAuth Use OAuth when your client should connect each user to their own Midpage account. Each user will need a Midpage account before they can sign in and authorize your client. For most customers, the easiest setup is: 1. Point your client at `https://app.midpage.ai/mcp/v1` 2. Let your MCP or OAuth library follow Midpage's discovery metadata and register a client automatically if it supports dynamic client registration 3. Let the user sign in and approve access when prompted Most MCP clients should not need pre-provisioned OAuth app credentials. Midpage's Clerk auth server supports dynamic client registration, so compatible clients can create their own credentials automatically. If your client cannot do that and needs a pre-provisioned OAuth app, contact Midpage. Need the manual OAuth settings? If your client supports discovery, start there. In most cases, pointing it at `https://app.midpage.ai/mcp/v1` is enough. Midpage publishes protected-resource metadata on `app.midpage.ai`, and that metadata points clients to the Clerk authorization server. The settings below are only for cases where you need to configure the OAuth flow yourself. **Discovery** * MCP protected-resource metadata: `https://app.midpage.ai/.well-known/oauth-protected-resource/mcp` * Clerk OAuth metadata: `https://clerk.midpage.ai/.well-known/oauth-authorization-server` * Clerk OIDC discovery: `https://clerk.midpage.ai/.well-known/openid-configuration` Some older MCP clients expect OAuth authorization-server metadata on the same origin as the MCP server. Midpage currently publishes protected-resource metadata on `app.midpage.ai` and authorization-server metadata on the Clerk domain above, so older clients may need manual configuration using those Clerk URLs. The current metadata advertises: * authorization server: `https://clerk.midpage.ai` * dynamic client registration endpoint: `https://clerk.midpage.ai/oauth/register` * PKCE challenge method: `S256` * resource scopes: `profile`, `email` **Which OAuth flow to use** * Public clients such as desktop apps, mobile apps, browser apps, and local CLIs should use Authorization Code + PKCE (`S256`). Do not embed a `client_secret` in those clients. If you need this mode, Midpage can provision your Clerk OAuth app as a public client. * Confidential server-side apps can use Authorization Code and exchange the code on their backend with either `client_secret_basic` or `client_secret_post`. * If you need refresh tokens for long-lived sessions, request `offline_access`. **Minimal flow** 1. Fetch the protected-resource metadata document and read `authorization_servers`. 2. Fetch the Clerk authorization-server or OIDC metadata document and use the published endpoints. 3. Start an authorization request with `response_type=code`, `client_id`, `redirect_uri`, `state`, and the `resource` value from the protected-resource metadata document. 4. Request `profile email` for MCP access. Add `openid` if your client expects an ID token. Add `offline_access` if you need a refresh token. 5. Public clients must also send `code_challenge` and `code_challenge_method=S256`. 6. Exchange the authorization `code` for tokens at the Clerk token endpoint, again including the same `resource` value. 7. Call the MCP server with `Authorization: Bearer `. **Example authorize request for a public client** ```text GET https://clerk.midpage.ai/oauth/authorize? response_type=code& client_id=YOUR_CLIENT_ID& redirect_uri=https://your-app.com/oauth/callback& resource=https%3A%2F%2Fapp.midpage.ai& scope=profile%20email%20openid%20offline_access& state=RANDOM_VALUE& code_challenge=BASE64URL_SHA256(code_verifier)& code_challenge_method=S256 ``` **Example token exchange for a public client** ```bash curl -X POST "https://clerk.midpage.ai/oauth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "client_id=YOUR_CLIENT_ID" \ -d "code=AUTHORIZATION_CODE" \ -d "redirect_uri=https://your-app.com/oauth/callback" \ -d "resource=https://app.midpage.ai" \ -d "code_verifier=YOUR_CODE_VERIFIER" ``` **Example token exchange for a confidential client** ```bash curl -X POST "https://clerk.midpage.ai/oauth/token" \ -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=AUTHORIZATION_CODE" \ -d "redirect_uri=https://your-app.com/oauth/callback" \ -d "resource=https://app.midpage.ai" ``` **Calling the MCP server** ```http Authorization: Bearer ``` ## Testing in Hosted MCP Clients If you just want to try Midpage in Claude, Codex, Perplexity, Cursor, or another hosted MCP client, add `https://app.midpage.ai/mcp/v1` and sign in with your Midpage account when prompted. You can create a free trial account at [app.midpage.ai](https://app.midpage.ai). For production integrations, use API key auth or OAuth as described above. ## Available Tools ### search Search US case law across federal and state courts. Returns metadata, highlights, and citator treatment data. **Parameters:** | Parameter | Type | Required | Description | | ---------------------------- | --------- | -------- | ---------------------------------------------------------------------------------------------------------- | | `queries` | array | Yes | Array of search queries (1-4) run in parallel | | `queries[].query` | string | Yes | Search query | | `queries[].startDate` | string | No | Start date (inclusive, YYYY-MM-DD) | | `queries[].endDate` | string | No | End date (inclusive, YYYY-MM-DD) | | `queries[].jurisdictionType` | string | No | `"federal"`, `"state"`, or `"state_and_federal"` | | `queries[].circuits` | string\[] | No | Federal circuits: `"1"`-`"11"`, `"dc"`, `"federal_circuit"`, `"supreme_court"` | | `queries[].states` | string\[] | No | State names (e.g., `["California", "New York"]`) | | `queries[].publishStatus` | string | No | `"published"`, `"unpublished"`, `"unknown"`, `"in_chambers"`, `"separate"`, `"errata"`, or `"relating_to"` | | `queries[].courts` | string\[] | No | Bluebook-style court abbreviations to narrow the search, such as `["S.D.N.Y.", "9th Cir."]` | If you filter on `publishStatus`, consider also running a parallel query with `"unknown"` because some jurisdictions have missing or uncertain publication metadata. **Example:** ```json { "queries": [ { "query": "breach of fiduciary duty elements", "jurisdictionType": "state_and_federal", "states": ["California"], "publishStatus": "published" }, { "query": "breach of fiduciary duty elements", "jurisdictionType": "state_and_federal", "states": ["California"], "publishStatus": "unknown" } ] } ``` --- ### findInOpinion Find quotable passages within a single opinion using keyword search. **Parameters:** | Parameter | Type | Required | Description | | -------------------------- | ------ | -------- | ----------------------------------------------------- | | `opinionId` | string | No\* | Midpage document ID (e.g., `"7228818"`) | | `reporterCitation` | string | No\* | Bluebook citation (e.g., `"556 U.S. 662"`) | | `docket` | object | No\* | Court and docket number pair | | `docket.courtAbbreviation` | string | Yes | Court abbreviation (e.g., `"S.D.N.Y."`, `"9th Cir."`) | | `docket.docketNumber` | string | Yes | Docket number (e.g., `"12-cv-20100"`) | | `query` | string | Yes | Key terms to match | \* Provide one of `opinionId`, `reporterCitation`, or `docket` **Example:** ```json { "opinionId": "7228818", "query": "minimum contacts purposeful availment" } ``` --- ### analyzeOpinion Reads the full document, then answers your question and returns the answer alongside text excerpts. Accepts either `opinionId`, `reporterCitation`, or `docket` as input. **Parameters:** | Parameter | Type | Required | Description | | -------------------------- | ------ | -------- | ----------------------------------------------------- | | `opinionId` | string | No\* | Midpage document ID | | `reporterCitation` | string | No\* | Bluebook citation (e.g., `"556 U.S. 662"`) | | `docket` | object | No\* | Court and docket number pair | | `docket.courtAbbreviation` | string | Yes | Court abbreviation (e.g., `"S.D.N.Y."`, `"9th Cir."`) | | `docket.docketNumber` | string | Yes | Docket number (e.g., `"12-cv-20100"`) | | `question` | string | Yes | The legal question you want answered | \* Provide one of `opinionId`, `reporterCitation`, or `docket` **Example:** ```json { "reporterCitation": "556 U.S. 662", "question": "What standard does this case establish for pleading requirements?" } ``` **Output fields:** | Field | Type | Description | | ------------------------------------- | --------- | -------------------------------------------------------------------------- | | `opinionId` | string | Midpage opinion ID | | `question` | string | The question that was analyzed | | `citation` | string | Bluebook citation for the opinion | | `url` | string | General Midpage document URL | | `supportedPropositions` | array | Propositions the opinion actually supports | | `supportedPropositions[].proposition` | string | Cite-ready proposition supported by the quote | | `supportedPropositions[].quote` | string | Verbatim supporting text from the opinion | | `supportedPropositions[].scope` | string | Limits, conditions, or context for the proposition | | `supportedPropositions[].centrality` | string | `core_holding`, `supporting_analysis`, `secondary_matter`, or `background` | | `supportedPropositions[].pinCiteUrl` | string | Midpage URL linked to the supporting line range | | `doesNotAddress` | string\[] | Topics from the question the opinion does not address | | `summary` | string | Short summary of what the case is about | | `jurisdiction` | string | Court and jurisdiction | | `treatment` | object | Citator treatment summary | **Example output:** ```json { "opinionId": "7228818", "question": "What does this case hold?", "citation": "Example v. Case, 1 F.4th 1 (9th Cir. 2024)", "url": "https://app.midpage.ai/document/7228818", "supportedPropositions": [ { "proposition": "The court adopted the rule.", "quote": "We adopt the rule.", "scope": "Applies in the stated procedural posture.", "centrality": "core_holding", "pinCiteUrl": "https://app.midpage.ai/document/7228818?lines=10-12" } ], "doesNotAddress": [], "summary": "The court adopted a rule.", "jurisdiction": "9th Circuit", "treatment": { "status": "positive", "citedBy": 10 } } ``` # MCP Changelog Midpage MCP versioned endpoints let integrations pin a tool contract: - [MCP v4](/documentation/integration/mcp-tools): `https://app.midpage.ai/mcp/v4` (current) - [MCP v3](/documentation/integration/mcp-tools/v3): `https://app.midpage.ai/mcp/v3` - [MCP v2](/documentation/integration/mcp-tools/v2): `https://app.midpage.ai/mcp/v2` - [MCP v1](/documentation/integration/mcp-tools/v1): `https://app.midpage.ai/mcp/v1` The unversioned endpoint, `https://app.midpage.ai/mcp`, serves the current contract, v4. ## v4 V4 exposes `search`, `analyzeCaseDocument`, `analyzeCaseDocket`, `searchLaws`, and `analyzeLaw`. - `search` merges up to four queries into one deduplicated result set, with composable court/state/date filters, exact document types, publication-status filtering for opinions, partial-result handling, and cursor continuation. - `analyzeCaseDocument` handles both opinions and filings, returning qualified propositions, source-extracted quotes, reader links, source warnings, and the Negative or Caution citing opinions behind a treatment flag. It replaces `analyzeOpinion`, `findInOpinion`, and `analyzeDocketFiling` in v4. - `analyzeCaseDocket` replaces `analyzeDocketReport`, answering from metadata and recorded activity while exposing coverage, freshness, and reusable filing references. `case.url` is the reader/citation link; `docketReport.downloadUrl` is the report download. - The laws tools retain their v3 contracts. V3 docket tools keep their premium gate; v4 can read stored material without that upfront gate, while PACER acquisition remains metered. - Opinion `caseName` and `citation` in `search` and `analyzeCaseDocument` use the opinion's short title when known, otherwise the docket caption, and `document.opinion` adds `perCuriam` and `enBanc`, each present only when `true`. Reader URLs are unchanged (added 2026-09-25). - `document.opinion` in `search` and `analyzeCaseDocument` adds `precedentialNote`, the court's limiting publication, precedential, or finality caveat, verbatim, present only when the opinion prints one (added 2026-10-09). See the [v4 reference and migration guide](/documentation/integration/mcp-tools#migrate-from-v3). ## v3 MCP v3 adds federal docket tools and laws tools: - `analyzeDocketReport` - `analyzeDocketFiling` - `analyzeLaw` — question-driven analysis of statutes, regulations, constitutions, and agency guidance by citation/register citation/path/id, returning verified verbatim passages, with current-law resolution, children navigation, and version chains (added 2026-07-04) - `searchLaws` — full-text search over the laws corpus with state/type/code filters and per-state hit counts for surveys (added 2026-07-05) The opinion research tools are unchanged from v2: - `search` - `findInOpinion` - `analyzeOpinion` At the v3 release, the unversioned endpoint moved to v3. Integrations that only need the stable opinion research contract can stay pinned to `https://app.midpage.ai/mcp/v2`. ## v2 MCP v2 keeps the same three tools: - `search` - `findInOpinion` - `analyzeOpinion` The `search` and `findInOpinion` tool contracts are unchanged. `analyzeOpinion` changes its output contract: | v1 | v2 | | --- | --- | | `supportedPropositions[].pinCiteUrl` | `supportedPropositions[].deeplinkURL` | | No `disposition` field | Required `disposition` field | | No `supportedPropositions[].opinionSection` field | Optional `supportedPropositions[].opinionSection` field | Use the [MCP v2 docs](/documentation/integration/mcp-tools/v2) for the complete v2 contract. ## v1 MCP v1 is the original stable contract. Use [MCP v1](/documentation/integration/mcp-tools/v1) if your integration depends on `supportedPropositions[].pinCiteUrl` or has not yet migrated to v2. # Cases API HTTP endpoints for case-law research: court opinions, courts, search, dockets, and filings. > One API covers both case law and laws (statutes, regulations, constitutions, agency guidance). The base URL and authentication below apply to all of it; the laws endpoints are documented in [Laws API](/documentation/integration/laws-api). ## Base URL ``` https://app.midpage.ai/api/v1 ``` ## Authentication Create and manage API keys in the [API dashboard](https://app.midpage.ai/developers). If you need help setting up an organization eligible for API keys, contact support@midpage.ai. `GET /me` also supports OAuth access tokens via `Authorization: Bearer `. ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/search` | POST | Search for opinions using semantic, keyword, or hybrid search | | `/opinions/get` | POST | Retrieve full opinion data by ID, citation, or docket | | `/court/get` | POST | Look up court metadata by ID, abbreviation, or name | | `/court/list` | GET | List every available court and its ID | | `/dockets/get` | POST | [preview] Retrieve a docket and its entries. Access upon request | | `/filings/get` | POST | [preview] Retrieve one filing's text and file. Access upon request | | `/laws/search` | POST | Search statutes, regulations, constitutions, and guidance | | `/laws/get` | POST | Retrieve one law document by citation, register citation, path, or id | | `/laws/collections` | GET | List available law collections (codes/registers) | | `/me` | GET | Retrieve the authenticated user ID and subscription status | The [preview docket and filing endpoints](#dockets-and-filings-preview) are available upon request. Contact [support@midpage.ai](mailto:support@midpage.ai) for access. ## Search Endpoint The `/search` endpoint supports three search modes: ### Semantic Search (default) Vector similarity search using AI embeddings. Best for conceptual queries. ```json { "query": "breach of fiduciary duty", "mode": "semantic", "page": 1, "page_size": 10 } ``` ### Keyword Search Traditional full-text search. Best for exact phrases and known terms. Supports Lexis/Westlaw-style boolean operators. ```json { "query": "summary judgment standard", "mode": "keyword", "page": 1, "page_size": 20, "filters": { "jurisdictions": ["Federal Appellate"], "publish_status": "published", "date_filed": { "start": "2020-01-01", "end": "2024-12-31" } } } ``` #### Boolean Operators Keyword mode supports Lexis/Westlaw-style boolean operators: | Operator | Description | Example | |----------|-------------|---------| | `AND` | Both terms must appear | `negligence AND damages` | | `OR` | Either term may appear | `breach OR default` | | `NOT` | Exclude term | `contract NOT employment` | | `"..."` | Exact phrase | `"summary judgment"` | | `*` | Wildcard (any characters) | `negligen*` | | `?` | Single character wildcard | `wom?n` | | `W/n` | Within n words (proximity) | `negligence W/5 damages` | | `()` | Grouping | `(contract OR agreement) AND breach` | **Important:** Boolean operators must be UPPERCASE (e.g., `AND` not `and`). **Example: Boolean search** ```json { "query": "(contract OR agreement) AND breach NOT employment", "mode": "keyword", "page": 1, "page_size": 20 } ``` **Example: Proximity search** ```json { "query": "negligence W/5 damages", "mode": "keyword", "page": 1, "page_size": 20 } ``` When boolean operators are detected, the response includes a `boolean_query` field in metadata: ```json { "metadata": { "mode": "keyword", "query": "negligence AND damages", "processing_time_ms": 459, "boolean_query": { "detected": true, "operators": ["AND"] } } } ``` ### Hybrid Search Combines semantic and keyword results using reciprocal rank fusion (RRF) with deduplication. Best for comprehensive coverage. ```json { "query": "fourth amendment search and seizure", "mode": "hybrid", "page": 1, "page_size": 10, "filters": { "court_ids": ["ca9", "scotus"] } } ``` ### Available Filters | Filter | Type | Description | |--------|------|-------------| | `court_ids` | string[] | Court identifiers (e.g., `["ca9", "scotus"]`) | | `jurisdictions` | string[] | Jurisdiction types (e.g., `["Federal Appellate"]`) | | `states` | string[] | State names (e.g., `["California"]`) | | `publish_status` | string | Publication status: `published`, `unpublished`, `unknown`, `in_chambers`, `separate`, `errata`, or `relating_to` | | `date_filed.start` | string | Start date (YYYY-MM-DD) | | `date_filed.end` | string | End date (YYYY-MM-DD) | Use `publish_status: "unknown"` to include opinions where publication metadata is missing or uncertain. ### Reranking Enable AI-powered reranking to improve result relevance using Cohere's rerank model. When enabled, the API fetches more candidates internally and reranks them for better precision. ```json { "query": "summary judgment standard", "mode": "keyword", "rerank": true, "page_size": 10 } ``` **Response with reranking:** ```json { "results": [...], "metadata": { "mode": "keyword", "query": "summary judgment standard", "processing_time_ms": 4328, "rerank": { "enabled": true, "rerank_time_ms": 344 } } } ``` **Notes:** - Reranking adds latency but significantly improves relevance - Cannot be used with `page > 1` (reranking only applies to first page) - Works with all search modes (semantic, keyword, hybrid) ### Facets (Result Breakdowns) For `keyword` and `hybrid` modes, the response can include facet breakdowns showing result counts by court, jurisdiction, state, and year when `include_facets: true` is set. This is useful for building filter UIs or understanding result distribution. Facet counts are computed across the full result set (after filters), not just the current page. To enable facets, set `include_facets: true`: ```json { "query": "copyright infringement", "mode": "keyword", "include_facets": true } ``` **Response with facets (when enabled):** ```json { "results": [...], "pagination": {...}, "facets": { "court_abbreviation": [ { "key": "9th Cir.", "count": 1523 }, { "key": "S.D.N.Y.", "count": 892 } ], "jurisdiction": [ { "key": "Federal Appellate", "count": 3200 }, { "key": "Federal District", "count": 4100 } ], "state": [ { "key": "California", "count": 1800 }, { "key": "New York", "count": 1650 } ], "year_filed": [ { "key": "2024", "count": 1200 }, { "key": "2023", "count": 2100 } ] } } ``` **Important notes:** - Facets are disabled by default and only included when `include_facets: true` - Facets are not available in `semantic` mode (Pinecone doesn't support aggregations) - In `hybrid` mode, facet counts reflect keyword search results only; semantic (Pinecone) results are not included in these counts. A `note` field will be present explaining this limitation. - Each result includes a `source` field indicating where it came from. In `semantic` and `keyword` modes it's constant; in `hybrid` mode it varies per item. ### Response Examples below are real responses with snippets truncated for brevity. **Semantic mode response (no facets):** ```json { "results": [ { "opinion_id": "8515707", "score": 0.721916258, "case_name": "Myer v. Preferred Credit, Inc.", "midpage_url": "https://app.midpage.ai/document/myer-v-preferred-credit-inc-8515707", "court_id": "ohctcomplharris", "court_name": "Harrison County Court of Common Pleas", "jurisdiction": "State Trial", "state": "Arizona", "date_filed": null, "snippet": "As such, it is a successor-in-interest, and may be liable on the claims herein...", "source": "semantic" } ], "pagination": { "page": 1, "page_size": 1, "total_results": 3, "total_pages": 3, "has_next": true, "has_prev": false }, "metadata": { "mode": "semantic", "query": "breach of fiduciary duty", "processing_time_ms": 182 } } ``` **Keyword mode response** (facets included by default): ```json { "results": [ { "opinion_id": "1000460566592", "score": 61.955864, "case_name": "In Re: OpenAI, Inc. Copyright Infringement Litigation", "midpage_url": "https://app.midpage.ai/document/in-re-openai-inc-copyright-1000460566592", "court_id": "nysd", "court_name": "District Court, S.D. New York", "court_abbreviation": "S.D.N.Y.", "jurisdiction": "Federal District", "state": "New York", "publish_status": "published", "date_filed": "2025-12-02", "docket_number": "1:23-cv-11195", "snippet": "INFRINGEMENT LITIGATION, ...", "source": "keyword" } ], "pagination": { "page": 1, "page_size": 1, "total_results": 10000, "total_pages": 10000, "has_next": true, "has_prev": false }, "metadata": { "mode": "keyword", "query": "copyright infringement", "processing_time_ms": 189 }, "facets": { "court_abbreviation": [ { "key": "S.D.N.Y.", "count": 11657 }, { "key": "N.D. Cal.", "count": 7993 } ], "jurisdiction": [ { "key": "Federal District", "count": 127544 }, { "key": "Federal Appellate", "count": 45956 } ], "state": [ { "key": "Federal", "count": 56818 }, { "key": "California", "count": 19113 } ], "year_filed": [ { "key": "1786", "count": 1 }, { "key": "1789", "count": 1 } ] } } ``` **Hybrid mode response** (mixed sources; facets include `note`): ```json { "results": [ { "opinion_id": "8515707", "score": 0.721916258, "case_name": "Myer v. Preferred Credit, Inc.", "midpage_url": "https://app.midpage.ai/document/myer-v-preferred-credit-inc-8515707", "court_id": "ohctcomplharris", "court_name": "Harrison County Court of Common Pleas", "jurisdiction": "State Trial", "state": "Arizona", "date_filed": null, "snippet": "As such, it is a successor-in-interest, and may be liable on the claims herein...", "source": "semantic" }, { "opinion_id": "10612453", "score": 35.931553, "case_name": "Salvatoriello v. Coady", "midpage_url": "https://app.midpage.ai/document/salvatoriello-v-coady-10612453", "court_id": "nysupctkings", "court_name": "New York Supreme Court, Kings County", "jurisdiction": "State Trial", "state": "New York", "date_filed": "2024-10-08", "snippet": "The verifj_ed amended_ complaint asserts causes of action for breach of fiduciary duty...", "source": "keyword" } ], "pagination": { "page": 1, "page_size": 2, "total_results": 10000, "total_pages": 5000, "has_next": true, "has_prev": false }, "metadata": { "mode": "hybrid", "query": "breach of fiduciary duty", "processing_time_ms": 196 }, "facets": { "court_abbreviation": [ { "key": "N.Y. App. Div.", "count": 77644 }, { "key": "Cal. Ct. App.", "count": 63785 } ], "jurisdiction": [ { "key": "Federal District", "count": 828389 }, { "key": "State Appellate", "count": 762503 } ], "state": [ { "key": "Federal", "count": 445751 }, { "key": "New York", "count": 177305 } ], "year_filed": [ { "key": "1700", "count": 2 }, { "key": "1701", "count": 1 } ], "note": "Facet counts reflect keyword search results only; semantic (Pinecone) results are not included in these counts." } } ``` Each search result includes `midpage_url`, the canonical public Midpage opinion page. Use this URL for links shown to end users; do not construct a `/document/{opinion_id}` URL, because bare opinion-ID pages are not public. --- ## Opinions Endpoint The `/opinions/get` endpoint retrieves full opinion data. Provide exactly one of: `opinion_ids`, `citations`, or `docket`. ### By Opinion IDs ```json { "opinion_ids": ["108713", "4976599"], "include_content": true, "include_detailed_treatments": false } ``` ### By Citation ```json { "citations": ["410 U.S. 113", "93 S. Ct. 705"] } ``` ### By Docket Number Requires court context (either `court_id` or `court_abbreviation`): ```json { "docket": { "docket_number": "70-18", "court_abbreviation": "SCOTUS" } } ``` ### Request Parameters | Parameter | Type | Description | |-----------|------|-------------| | `opinion_ids` | string[] | Opinion IDs to retrieve (max 100) | | `citations` | string[] | Citations to look up (max 100) | | `docket` | object | Docket lookup with `docket_number` and `court_id` or `court_abbreviation` | | `include_content` | boolean | Include full HTML opinion text (default: false) | | `include_detailed_treatments` | boolean | Include citing opinions with treatment analysis (default: false) | ### Response ```json { "opinions": [ { "id": "108713", "case_name": "Roe v. Wade", "midpage_url": "https://app.midpage.ai/document/roe-v-wade-108713", "court_id": "scotus", "court_abbreviation": "SCOTUS", "docket_number": "70-18", "state": "Federal", "publish_status": "published", "date_filed": "1973-01-22", "judge_name": "Blackmun", "citations": [ { "cited_as": "410 U.S. 113", "volume": "410", "reporter": "U.S.", "page": "113" } ], "pagination_citation": "410 U.S. 113", "citation_count": 4603, "overall_treatment": "Negative", "html_content": "...", "treatments": [ { "citing_id": "12345", "treatment_category": "Negative", "treatment_description": "Overruled by...", "is_authoritative": true, "supporting_quote": "..." } ] } ], "citation_matches": [ { "citation": "410 U.S. 113", "opinion_id": "108713" } ] } ``` Every returned opinion includes `midpage_url`, the canonical public Midpage opinion page suitable for sharing with unauthenticated users. `pagination_citation` names the citation whose page numbers the `` page markers in `html_content` follow. An opinion often has several citations, but its text usually carries the page breaks of only one reporter, so use this citation when building pinpoint citations. It is `null` when the text has no reporter page markers or the reporter cannot be established; cite without a pinpoint page then. It is returned whether or not `include_content` is set. See [Page Numbers](/documentation/data-model/content-format#page-numbers). `case_name` is the opinion's short title when the document carries one, otherwise the docket caption. For example, the caption "Lydia Tummel v. Robert Milane" is returned as "Tummel v. Milane". The `midpage_url` slug is always built from the docket caption, so it can differ from `case_name`. `judge_name` lists every known judge on the opinion, comma-separated (for example `"KING, JONES, DENNIS"`). It is always returned, and is `null` when no judge is known. `per_curiam` and `en_banc` appear only when they are `true`. When an opinion is not per curiam, or that is unknown, the key is absent; it is never `false` or `null`. `en_banc` works the same way. `precedential_note` is the court's own limiting caveat printed on the opinion, verbatim. It is either a publication or precedential limit, such as `"NOT FOR PUBLICATION"` or a court rule stating that the opinion is not precedent, or a finality caveat, such as `"not final until disposition of any timely motion for rehearing"`. Several caveats are joined with `" | "`. Positive designations such as `"FOR PUBLICATION"` are never included. It is independent of `publish_status`. A missing `precedential_note` does not prove the opinion prints no caveat. Coverage is incomplete: some older opinions were never checked for one, and some carry only their strongest caveat, shortened. Use `publish_status` for the publication decision. These rules apply to `/opinions/get`. `/search` results are built from the search index and do not include `per_curiam`, `en_banc`, or `precedential_note`. --- ## Court Endpoint The `/court/get` endpoint retrieves court metadata. Provide exactly one of: `court_ids`, `court_abbreviations`, or `names`. ### Request ```json { "court_abbreviations": ["SCOTUS", "9th Cir."] } ``` ### Response ```json { "courts": [ { "id": "scotus", "full_name": "Supreme Court of the United States", "court_abbreviation": "SCOTUS", "jurisdiction": "Federal Appellate", "state": "Federal", "in_use": true, "pacer_court_id": null }, { "id": "ca9", "full_name": "Court of Appeals for the Ninth Circuit", "court_abbreviation": "9th Cir.", "jurisdiction": "Federal Appellate", "state": "Federal", "in_use": true, "pacer_court_id": "9" } ] } ``` --- ## Court List Endpoint `GET /court/list` returns every court available through the API, with the same fields as `/court/get`. It takes no parameters. Use it to find the IDs that the `court_ids` search filter accepts, and to see when courts are added. `in_use` is `false` for historical courts, such as courts that were renamed, merged, or abolished, and `true` for current courts. Opinions from historical courts remain available. ### Response ```json { "courts": [ { "id": "nlrb", "full_name": "National Labor Relations Board", "court_abbreviation": "NLRB", "jurisdiction": "Federal Special", "state": "Federal", "in_use": true, "pacer_court_id": null }, { "id": "ccpa", "full_name": "Court of Customs and Patent Appeals", "court_abbreviation": "C.C.P.A.", "jurisdiction": "Federal Special", "state": "Federal", "in_use": false, "pacer_court_id": null } ] } ``` ## Dockets and filings [preview] Access is available upon request. Contact [support@midpage.ai](mailto:support@midpage.ai) to request access. These endpoints use the same base URL and require an organization API key with Full access. Read the federal and state court dockets and filings we hold. With On-demand PACER enabled, you can also fetch federal dockets and filings we do not hold. | Endpoint | Method | Purpose | |---|---|---| | `/api/v1/dockets/get` | POST | A docket: case details, parties and entries | | `/api/v1/filings/get` | POST | One filing: its text and a link to the file | ### PACER balance Dockets and filings served from storage are free. For a federal docket or filing we don't have, or a fresher copy asked for with `max_age_hours`, we may fetch it from PACER. PACER fetches are paid from your organization's prepaid PACER balance, at PACER's fee with no markup. - To turn on PACER fetches for your organization, contact [support@midpage.ai](mailto:support@midpage.ai). Without them, requests that need a fetch answer `403 pacer_not_enabled`. - Org admins top up the balance from the Billing tab of the [developer portal](https://app.midpage.ai/developers). The minimum top-up is $50. - When the balance is too low for a fetch, the request answers `402 pacer_balance_insufficient`. Successful responses include `cost`. Some error responses include it too: ```json "cost": { "pacer_cents": 30, "pacer_pages": 3, "balance_cents": 46970 } ``` `pacer_cents` is what this request was charged, `pacer_pages` the pages PACER billed, and `balance_cents` your available balance after it. `pacer_pages` and `balance_cents` can be `null` when unavailable. Authentication, validation, and other early errors can omit `cost`. Before a purchase, your available balance must cover a hold of $3.00 for a district or bankruptcy docket, $3.50 for an appellate docket, $6.00 for an appellate filing, or the quoted fee for a district or bankruptcy filing. The final charge is PACER's fee, capped at the hold. A filing request can first buy a docket. If that succeeds but the filing cannot be returned, the docket remains available and charged, including on a `402` or `502` response. Always check `cost` when handling an error. Retrying `max_age_hours: 0` can buy another refresh; these endpoints do not accept an idempotency key. ### Get a docket `POST /api/v1/dockets/get` ```json { "docket": { "docket_number": "1:24-cv-00533", "court_id": "nysd" }, "max_age_hours": 24 } ``` | Parameter | Type | Description | |---|---|---| | `case_id` | string | A docket's `case_id`. Send this or `docket` | | `docket.docket_number` | string | The court's docket number | | `docket.court_id` | string | Court ID, such as `nysd`. Or send `docket.court_abbreviation` | | `docket.case_name` | string | Optional. Picks one docket when several share the number | | `max_age_hours` | integer | Optional, 0 or greater. At courts PACER serves, fetch again if our copy is older than this many hours. `0` always fetches on the first page. Omit to accept any held copy | | `limit` | integer | Entries per page, 1 to 1000. Default 1000 | | `cursor` | string | `next_cursor` from the previous page. Repeat the original `case_id` or `docket` lookup | At courts PACER does not serve, `max_age_hours` is ignored; `fetched_at` gives the copy's age when known. Cursor pages are free and do not refresh the docket. Keep using the original lookup when paging, even if the response returns a different `case_id`. Response excerpt: ```json { "docket": { "case_id": "5c1f9e2a-7d43-4b8e-9a61-2f0c8d3e4b17", "case_name": "Doe v. Acme Corp.", "court_id": "nysd", "docket_number": "1:24-cv-00533", "date_filed": "2024-01-24", "parties": [{ "name": "Acme Corp.", "role": "Defendant", "attorneys": [] }], "fetched_at": "2026-10-14T13:02:11.000Z", "entry_count": 1 }, "entries": [ { "filing_id": "c9f1a2b3-4d5e-4f60-8a7b-9c0d1e2f3a4b", "entry_number": "42", "date_filed": "2026-09-30", "description": "MOTION to Dismiss for Lack of Jurisdiction", "document": "purchasable", "attachments": [] } ], "next_cursor": null, "cost": { "pacer_cents": 30, "pacer_pages": 3, "balance_cents": 46970 } } ``` Each entry's `document` is `stored` (free from `filings/get`), `purchasable` (can be fetched from PACER with access enabled) or `none`. Attachments have their own `filing_id`. On appellate dockets, `entry_number` can be `null`; use `filing_id` instead. The [API Reference](/api-reference) lists every field. ### Get a filing `POST /api/v1/filings/get`. Send `case_id` and one of `filing_id` or `entry_number`. ```json { "case_id": "5c1f9e2a-7d43-4b8e-9a61-2f0c8d3e4b17", "filing_id": "c9f1a2b3-4d5e-4f60-8a7b-9c0d1e2f3a4b" } ``` | Parameter | Type | Description | |---|---|---| | `case_id` | string | The docket's `case_id` | | `filing_id` | string | An entry's or attachment's `filing_id` from `dockets/get` | | `entry_number` | string | The entry number as printed, such as `"42"` | | `attachment_number` | string | Optional, with `entry_number`. `"1"` for ECF No. 42-1 | Entry and attachment numbers are strings, such as `"42"` and `"1"`. Response excerpt: ```json { "filing": { "filing_id": "c9f1a2b3-4d5e-4f60-8a7b-9c0d1e2f3a4b", "case_id": "5c1f9e2a-7d43-4b8e-9a61-2f0c8d3e4b17", "entry_number": "42", "description": "MOTION to Dismiss for Lack of Jurisdiction", "content_type": "application/pdf", "file_url": "https://app.midpage.ai/api/case/5c1f9e2a-7d43-4b8e-9a61-2f0c8d3e4b17/docket/c9f1a2b3-4d5e-4f60-8a7b-9c0d1e2f3a4b/raw", "text": "UNITED STATES DISTRICT COURT\nSOUTHERN DISTRICT OF NEW YORK\n..." }, "cost": { "pacer_cents": 40, "pacer_pages": 4, "balance_cents": 46930 } } ``` `text`, `file_url`, and occasionally `filing_id` can be `null` when unavailable, even after a successful purchase. A missing field value does not reverse the charge. ### Docket and filing errors Errors carry `error` and `code`, and usually a `message` for people. Branch on `code`. Both endpoints share a rate of 30 requests per minute per organization, across all its keys. | Status | `code` | When | |---|---|---| | 400 | `invalid_request` | The request isn't valid | | 401 | `unauthorized` | The API key is missing or invalid | | 402 | `pacer_balance_insufficient` | The PACER balance is too low for this fetch | | 403 | `pacer_not_enabled` | The request needs a PACER fetch and PACER fetches aren't on for your organization, or the key doesn't have Full access | | 403 | `filing_restricted` | The court restricts access to this document | | 404 | `docket_not_found`, `filing_not_found`, `no_document` | No such docket or filing, or no document we can return | | 409 | `ambiguous_docket` | Several dockets share this number. Resend with a candidate's `case_id`, or `docket.case_name` | | 409 | `ambiguous_filing` | Several entries print this number. Use `filing_id` | | 422 | `unsupported_court` | We don't know this court, or the filing needs a fetch from a court PACER doesn't cover | | 429 | `too_many_requests` | More than 30 requests a minute. Wait the seconds in `Retry-After` | | 500 | `internal_error` | An error on our side | | 502 | `pacer_unavailable` | PACER or the court didn't answer. Try again later | | 503 | `service_unavailable` | Briefly unavailable. Wait the seconds in `Retry-After` | --- ## Current User Endpoint Use `GET /me` to fetch the authenticated user's Midpage user ID and subscription status. ### Response ```json { "userId": "user_123", "subscriptionStatus": "active" } ``` Common `subscriptionStatus` values: - `active` - `trialing` - `past_due` - `canceled` --- See the **[API Reference](/api-reference)** tab for complete endpoint documentation, request/response schemas, and interactive examples. # Laws API The Data Services API also covers **statutes, regulations, constitutions, and agency guidance** — US federal plus all 50 states and DC — as structured, citable documents with verbatim text. Same API, same base URL, same bearer API key as the case-law endpoints; laws add three endpoints: | Endpoint | Method | Purpose | |---|---|---| | `/api/v1/laws/search` | POST | Full-text search with jurisdiction/type/subtree filters | | `/api/v1/laws/get` | POST | Retrieve one document by citation, register citation, path, or id | | `/api/v1/laws/collections` | GET | Enumerate available codes/registers (filter values) | Authentication is the same bearer API key as the rest of the Data Services API. ## The document model Laws are stored as **trees**: each collection (a code or register, e.g. `cfr`, `uscode`, `ca-ccr`) is a hierarchy of documents — titles contain chapters contain parts contain sections. Every document has: - `id` — UUID, the stable handle for follow-up requests - `parent_id` — its container (fetch it to zoom out) - `path` — its ltree position within the collection (e.g. `cfr.t21.cI.scH.p820.spA.s820_1`); paths are unique **within** a collection only - `node_type` — `LEAF` (a content section), `PARENT` (a container), or `TOMBSTONE` (a repealed/reserved placeholder) - `citation` (canonical, e.g. `21 C.F.R. § 820.1`), `number`, `title` - `register_citation` — Federal Register / state register cite, for regulations that carry one - `html` — the verbatim text (containers usually have none); external link `href`s are stripped (anchor text and in-page `#` anchors survive) - `url` — the Midpage page for this exact version; `source_url` — the official source - `state` (`"Federal"` or a state name), `country`, `collection_type` ## Currency: what "current" means There is **no stored is_current flag** — currency is derived at request time. A document is current iff: 1. it is not marked historical (superseded by the source), 2. its `effective_date` is not in the future — a future-effective amendment is stored as a version but is *not yet* the law, 3. no newer version is already in force — until a future amendment's effective date arrives, the older text remains current ("anchor" semantics), and 4. it is not a `TOMBSTONE` — repeal/removal markers are lifecycle notes, never the law in force. Both endpoints filter to current law **by default**. `search` takes `current_only: false`, `get` takes `include_historical: true` to widen; every returned document carries `is_current` either way. The derivation reacts to the calendar automatically: on the amendment's effective day, the old version stops being current and the new one starts, with no data change required. Historical access requires an API key with historical entitlement: without it, the widening flags and historical-`id` lookups return 403 and `versions` comes back empty. ## Versions When a document has version history (statute amended, regulation revised), `get` returns a `versions` array: every version's `id`, `effective_date`, `publication_date`, plus `is_historical` / `is_current` flags, oldest first, with `is_requested` marking the one returned as `document`. To read an older (or future) version, `get` it by its `id` — an id lookup always returns that exact version (historical versions require full historical access). Note on dates: `effective_date` is only populated when the source states it. Several state codes publish session-year history rather than effective dates — those versions have `effective_date: null` and order by publication / observation dates instead. ## Navigating a code `get` on a container (`node_type: "PARENT"`) returns `children`: its current children in the code's authored order — navigation rows only (`id`, `number`, `citation`, `title`, `sort_order`; no text), capped at 250 with the true count in `children_total`. Typical drill-down: ``` get { citation: "21 C.F.R. Part 820" } → part container + section list get { id: } → the section text get { id: } → back up to the container ``` There is deliberately no children pagination: when a container has more than 250 children, walking the tree is the wrong tool — search directly for the target section (`query` + `collections` + `path_prefix`) and `get` the hit. ## Lookup methods on `get` Provide **exactly one** of: | Field | Use for | Notes | |---|---|---| | `citation` | canonical cites | insensitive to punctuation, spacing, and `§`/`sec.`/`section` style (`"42 USC 1983"` matches `"42 U.S.C. § 1983"`); wording must still match the canonical form — no abbreviation aliasing | | `register_citation` | FR / state register cites | regulations & rule documents | | `path` | tree positions | pair with `collection` (paths unique per collection) | | `id` | anything already seen | always unambiguous; returns that exact version regardless of currency | The optional `collection` field narrows path/citation/register-citation lookups and accepts either a collection id (`"cfr"`) or a human-readable name (`"Code of Federal Regulations"`), resolved server-side — an unresolvable value returns 400 with did-you-mean suggestions. Request bodies are strict: unknown keys return 400 rather than being silently ignored. If a citation matches several documents (rare, e.g. the same cite in two collections), the response has `status: "ambiguous"` and `matches` — candidate summaries; retry with one of the `id`s. A miss returns 404 with `status: "not_found"` and a `message` saying why — including the case where only non-current versions matched (retry with `include_historical: true` to retrieve those). ## Search filters `search` narrows with `states` (full names or USPS abbreviations, `"Federal"` for federal law — validated server-side, unrecognized values 400 with did-you-mean suggestions), `collection_types`, `collections` (ids or human-readable names, resolved the same way), `path_prefix` (a dot-separated ltree prefix, e.g. `"cfr.t21"` for all of 21 CFR — malformed prefixes 400 rather than matching nothing), and inclusive `effective_date_from/to` / `publication_date_from/to` ranges. Pagination is `page` / `page_size` (max 50); results beyond the first 10,000 cannot be paginated — narrow the query instead. Each hit carries the document's identifying fields, dates, currency flags, highlighted `snippets`, and `url` (the Midpage page for that exact version). A page can hold fewer than `page_size` results; `total` is an estimate for the whole query. ## State surveys The search response includes `state_counts`: estimated per-state hit counts for the whole query, not just the returned page — use them to scope a survey, then verify each state with a filtered search. A 5-state survey is two steps: ```jsonc // 1. Where is this regulated, and how much? POST /api/v1/laws/search { "query": "telehealth prescribing controlled substances", "collection_types": ["regulation"] } // → state_counts: { "California": 12, "Texas": 8, ... } // 2. Per state: top provisions, then `get` each by id for verbatim text. POST /api/v1/laws/search { "query": "telehealth prescribing controlled substances", "collection_types": ["regulation"], "states": ["California"] } ``` `GET /api/v1/laws/collections` lists every collection with `state` and `type`, e.g. to confirm which states have a regulation collection before surveying. ## Relationship to the MCP laws tools The MCP server (latest version) exposes the same data layer as two tools: `searchLaws` (search + `stateCounts`, camelCase inputs, same `collections` / `states` resolution) and `analyzeLaw` (lookup by `citation` / `registerCitation` / `path` / `id` with the same children + versions model — but analysis-only: a `question` is required, the document text is analyzed internally, and the tool returns verified verbatim passages rather than raw text; use this API's `get` when you need the `html` itself). # Schema Complete reference for the database schemas. The database is organized into three schemas: - **`cases`** - Court opinions and caselaw data (current) - **`opinions`** - The previous shape of the same data. **Deprecated** — see [Migrating from `opinions` to `cases`](#migrating-from-opinions-to-cases) - **`laws`** - Statutes and regulations > **`cases` is where new work should go.** It carries everything `opinions` > carried, plus the full docket rather than opinions alone: roughly 17 million > additional docket entries and their document metadata. `opinions` continues > to work unchanged during the overlap window and will be withdrawn on a date > we will announce with notice. ## Cases Schema The `cases` schema is the current shape of our caselaw data. Where `opinions` carried opinions only, `cases` carries the **whole docket** of every case it publishes — motions, orders, notices and the rest — along with document metadata for each entry. Two structural differences are worth reading before you write queries: 1. **An opinion and a docket entry are now separate rows.** `opinions.docket_entries` was one wide table; it is now `cases.docket_entries` joined to `cases.opinions` on `docket_entries.opinion_id = opinions.id`. An entry with a `NULL` `opinion_id` is a non-opinion docket entry — the new corpus. 2. **Text is keyed by document, not by opinion.** `opinions.content` became `cases.documents` ⋈ `cases.documents_content`, joined on `documents_content.id = documents.id`. Every table carries `is_promoted`, which is always `true` in your replica — it is the flag that determines what reaches you at all. You can ignore it; it is present because our replication requires it. Every table also carries `deleted_at`. **Rows are not removed when deleted**; they are marked. Filter `WHERE deleted_at IS NULL` for live rows. See [Change Tracking](connection.md#change-tracking) for the one case this does not cover. ### Entity Relationship ``` courts └── cases └── docket_entries ──── opinions (entries with an opinion_id) └── documents └── documents_content opinions └── citations document_relations (citation graph) └── treatments (how the citing document treats the cited one) ``` ## Cases Tables ### cases.courts Court definitions for federal, state, territorial, tribal, military, and international jurisdictions. Use `in_use` to distinguish courts that are actively covered. | Column | Type | Description | |--------|------|-------------| | `id` 🔑 | VARCHAR(50) | Court identifier | | `pacer_court_id` | VARCHAR(10) | This court's PACER identifier, where it has one | | `full_name` | TEXT | Full court name | | `court_abbreviation` | VARCHAR(100) | Citation-form abbreviation | | `jurisdiction` | VARCHAR(50) | Jurisdiction | | `state` | VARCHAR(50) | State | | `country` | VARCHAR(2) | ISO country code | | `freelaw_court_id` | VARCHAR(50) | CourtListener / Free Law Project identifier | | `in_use` | BOOLEAN | Whether this court is actively covered | | `is_promoted` | BOOLEAN | Always `true` | | `deleted_at` | TIMESTAMPTZ | Soft-delete marker | | `created_at` | TIMESTAMPTZ | When this row first reached your replica | | `updated_at` | TIMESTAMPTZ | When this row last changed in your replica | `pacer_court_id` and `freelaw_court_id` are the join keys if you also license PACER/RECAP or CourtListener data. --- ### cases.cases One row per case. | Column | Type | Description | |--------|------|-------------| | `id` 🔑 | TEXT | Case identifier | | `court_id` → | VARCHAR(50) | The court | | `docket_number` | TEXT | Docket number | | `case_name` | TEXT | Case name | | `date_filed` | DATE | Date filed | | `date_terminated` | DATE | Date terminated, where known | | `source_id` | VARCHAR(50) | The source's own public identifier for this case (PACER case id, where applicable) | | `data_origin` | VARCHAR(25) | Which acquisition path produced this case | | `is_promoted` | BOOLEAN | Always `true` | | `deleted_at` | TIMESTAMPTZ | Soft-delete marker | | `created_at` | TIMESTAMPTZ | When this row first reached your replica | | `updated_at` | TIMESTAMPTZ | When this row last changed in your replica | --- ### cases.docket_entries One row per document filed in a case. This is the table that grew: it now includes non-opinion filings. | Column | Type | Description | |--------|------|-------------| | `case_id` 🔑 → | TEXT | The case | | `id` 🔑 | TEXT | Entry identifier | | `opinion_id` → | TEXT | The opinion this entry is, or `NULL` if it is not an opinion | | `source_id` | VARCHAR(50) | The source's own identifier for this entry | | `date_filed` | DATE | Date filed | | `entry_number` | BIGINT | Docket entry number | | `description` | TEXT | Docket text | | `data_origin` | VARCHAR(25) | Which acquisition path produced this entry | | `doc_source` | VARCHAR(50) | Where the document came from | | `is_promoted` | BOOLEAN | Always `true` | | `deleted_at` | TIMESTAMPTZ | Soft-delete marker | | `created_at` | TIMESTAMPTZ | When this row first reached your replica | | `updated_at` | TIMESTAMPTZ | When this row last changed in your replica | Two things to know: - **`opinion_id IS NOT NULL` is how you identify an opinion.** This replaces the old `is_opinion` boolean. - **`opinion_id` is not unique.** One opinion can be reached from several entries in several cases. If your code assumes one row per opinion, it needs a `DISTINCT` or a join through `cases.opinions`. The primary key is `(case_id, id)`. --- ### cases.opinions The opinion-specific attributes of a docket entry. | Column | Type | Description | |--------|------|-------------| | `id` 🔑 | TEXT | Opinion identifier. **Stable across the migration** — the same value the old `opinions.docket_entries.id` carried | | `judge_names` | TEXT[] | Authoring judges. An **array**, where the old schema had a single text field | | `opinion_role` | ENUM | `majority`, `concurrence`, `dissent`, `combined` | | `publish_status` | ENUM | `published`, `unpublished`, `in_chambers`, `separate`, `errata`, `relating_to`, `unknown` | | `per_curiam` | ENUM | `yes`, `no`, `unknown` | | `en_banc` | ENUM | `yes`, `no`, `unknown` | | `precedential_note` | TEXT | Precedential status note | | `is_promoted` | BOOLEAN | Always `true` | | `deleted_at` | TIMESTAMPTZ | Soft-delete marker | | `created_at` | TIMESTAMPTZ | When this row first reached your replica | | `updated_at` | TIMESTAMPTZ | When this row last changed in your replica | --- ### cases.documents Document metadata for a docket entry. | Column | Type | Description | |--------|------|-------------| | `id` 🔑 | TEXT | Document identifier | | `case_id` → | TEXT | The case | | `docket_entry_id` → | TEXT | The docket entry | | `document_type` | ENUM | `opinion`, `brief`, `motion`, `order`, `transcript`, `judgment`, `complaint`, and others | | `file_type` | VARCHAR(100) | File type | | `title` | TEXT | Document title | | `is_promoted` | BOOLEAN | Always `true` | | `deleted_at` | TIMESTAMPTZ | Soft-delete marker | | `created_at` | TIMESTAMPTZ | When this row first reached your replica | | `updated_at` | TIMESTAMPTZ | When this row last changed in your replica | --- ### cases.documents_content Full text, keyed by document. | Column | Type | Description | |--------|------|-------------| | `id` 🔑 → | TEXT | Document ID (same as `documents.id`) | | `html_content` | TEXT | Document text in HTML format | | `is_promoted` | BOOLEAN | Always `true` | | `deleted_at` | TIMESTAMPTZ | Soft-delete marker | | `created_at` | TIMESTAMPTZ | When this row first reached your replica | | `updated_at` | TIMESTAMPTZ | When this row last changed in your replica | --- ### cases.citations Citations extracted from opinions. | Column | Type | Description | |--------|------|-------------| | `id` 🔑 | UUID | Citation identifier | | `opinion_id` → | TEXT | The opinion this citation appears in | | `cited_as` | TEXT | The citation as presented | | `reporter` | VARCHAR(50) | Reporter | | `volume` | VARCHAR(20) | Volume | | `page` | VARCHAR(20) | Page | | `pincite` | INTEGER | Pinpoint page | | `year` | INTEGER | Year | | `citation_type` | VARCHAR(50) | Citation type | | `normalized` | VARCHAR(150) | Normalized form, for matching | | `court_id` → | VARCHAR(50) | The court | | `is_promoted` | BOOLEAN | Always `true` | | `deleted_at` | TIMESTAMPTZ | Soft-delete marker | | `created_at` | TIMESTAMPTZ | When this row first reached your replica | | `updated_at` | TIMESTAMPTZ | When this row last changed in your replica | --- ### cases.document_relations The citation graph: one row per citing→cited edge. Where the old schema put the edge and its treatment in one table, the edge lives here and the treatment in `cases.treatments`. | Column | Type | Description | |--------|------|-------------| | `id` 🔑 | UUID | Edge identifier | | `citing_table` | ENUM | What kind of thing is citing (`opinion` today) | | `citing_id` | TEXT | The document doing the citing | | `cited_table` | ENUM | What kind of thing is cited (`opinion` today) | | `cited_id` | TEXT | The document being cited | | `is_authoritative` | BOOLEAN | Whether the citation is authoritative. **`NULL` means not yet evaluated**, not "evaluated and found non-authoritative" | | `is_promoted` | BOOLEAN | Always `true` | | `deleted_at` | TIMESTAMPTZ | Soft-delete marker | | `created_at` | TIMESTAMPTZ | When this row first reached your replica | | `updated_at` | TIMESTAMPTZ | When this row last changed in your replica | `citing_table` and `cited_table` are `opinion` for every edge today. They exist so that citations to statutes and regulations can be added later without changing the shape of this table. --- ### cases.treatments How a citing document treats the document it cites. Joins to `document_relations` on `relation_id = document_relations.id`. | Column | Type | Description | |--------|------|-------------| | `relation_id` 🔑 → | UUID | The edge in `document_relations` | | `treatment_category` | TEXT | How the citing document treats the cited one | | `treatment_description` | TEXT | Detailed treatment explanation | | `supporting_quote` | TEXT | Quote from the citing document supporting the treatment | | `is_promoted` | BOOLEAN | Always `true` | | `deleted_at` | TIMESTAMPTZ | Soft-delete marker | | `created_at` | TIMESTAMPTZ | When this row first reached your replica | | `updated_at` | TIMESTAMPTZ | When this row last changed in your replica | 🔑 = Primary Key, → = Foreign Key --- ## Opinions Schema > **Deprecated.** Superseded by the [`cases` schema](#cases-schema). Still live > and still maintained during the overlap window; no date is set yet. The > mapping between the two is in > [Migrating from `opinions` to `cases`](#migrating-from-opinions-to-cases). The `opinions` schema contains normalized court opinion data. All tables are in the `opinions` schema and can be joined using foreign key relationships. ### Entity Relationship ``` courts └── cases └── docket_entries ├── citations └── content opinion_relations (cross-references between opinions) ``` ## Opinions Tables ### opinions.courts Court definitions for federal, state, territorial, tribal, military, and international jurisdictions. Use `in_use` to distinguish courts that are actively covered. | Column | Type | Description | |--------|------|-------------| | `id` 🔑 | VARCHAR(50) | Court identifier (e.g., 'ca9', 'nysd', 'scc') | | `full_name` | TEXT | Full court name | | `court_abbreviation` | VARCHAR(100) | Citation abbreviation | | `jurisdiction` | VARCHAR(50) | Federal Appellate, Federal District, Federal Bankruptcy, Federal Special, State Supreme, State Appellate, State Trial, State Special, State Attorney General, Military, Military Appellate, Tribal, U.S. Territory, Committee, International | | `state` | VARCHAR(50) | State/province name, 'Federal', 'Tribal', or territory name | | `country` | VARCHAR(2) | ISO 3166-1 alpha-2 country code (US, CA, etc.) | | `in_use` | BOOLEAN | Whether court is actively scraped | --- ### opinions.cases Case metadata including docket information and dates. | Column | Type | Description | |--------|------|-------------| | `id` 🔑 | TEXT | Internal case ID | | `court_id` → | VARCHAR(50) | Court identifier | | `docket_number` | TEXT | Court docket number | | `case_name` | TEXT | Case name/caption | | `date_filed` | DATE | Date case was filed | | `date_terminated` | DATE | Date case was terminated | | `data_origin` | VARCHAR(25) | Source of the data | --- ### opinions.docket_entries Opinion entries (where is_opinion = true). Each row represents a court opinion. | Column | Type | Description | |--------|------|-------------| | `id` 🔑 | TEXT | Opinion identifier | | `case_id` → | TEXT | Parent case | | `description` | TEXT | Opinion description | | `date_filed` | DATE | Opinion filing date | | `judge_name` | TEXT | Authoring judge name | | `is_opinion` | BOOLEAN | Whether this is an opinion (true for opinions) | | `opinion_role` | ENUM | Role of opinion (lead, concurrence, dissent, etc.) | | `publish_status` | ENUM | Publication status | | `data_origin` | VARCHAR(25) | Source of the data | --- ### opinions.citations Reporter citations for opinions. | Column | Type | Description | |--------|------|-------------| | `id` 🔑 | UUID | Internal citation ID | | `opinion_id` → | TEXT | Parent opinion | | `court_id` → | VARCHAR(255) | Court identifier | | `volume` | VARCHAR(20) | Reporter volume number | | `reporter` | VARCHAR(50) | Reporter abbreviation (e.g., 'U.S.', 'F.3d') | | `page` | VARCHAR(20) | Starting page number | | `cited_as` | TEXT | Full citation string | | `citation_type` | VARCHAR(50) | Type of citation | | `year` | INTEGER | Year of citation | | `normalized` | VARCHAR(150) | Normalized citation format | --- ### opinions.content Full text content for opinions. | Column | Type | Description | |--------|------|-------------| | `id` 🔑 | TEXT | Opinion ID (same as docket_entries.id) | | `html_content` | TEXT | Opinion text in HTML format | | `created_at` | TIMESTAMP | When content was added | --- ### opinions.opinion_relations Citation relationships and treatment analysis between opinions. | Column | Type | Description | |--------|------|-------------| | `cited_id` → | TEXT | The opinion being cited | | `citing_id` → | TEXT | The opinion doing the citing | | `citator_version` | TEXT | Version of citator analysis | | `is_authoritative` | BOOLEAN | Whether this is an authoritative citation | | `treatment_category` | TEXT | How the citing opinion treats the cited opinion | | `treatment_description` | TEXT | Detailed treatment explanation | | `supporting_quote` | TEXT | Quote from citing opinion supporting the treatment | 🔑 = Primary Key, → = Foreign Key --- ## Migrating from `opinions` to `cases` Everything `opinions` gave you is available in `cases`. Most of the work is mechanical; three changes need a real edit. | `opinions` | `cases` | Note | |---|---|---| | `opinions.docket_entries` (opinions only) | `cases.opinions` ⋈ `cases.docket_entries` | One table becomes two. Join on `docket_entries.opinion_id = opinions.id` | | `docket_entries.is_opinion` | `docket_entries.opinion_id IS NOT NULL` | Derived rather than stored | | `docket_entries.judge_name` (TEXT) | `opinions.judge_names` (TEXT[]) | Type change | | `opinions.content` | `cases.documents` ⋈ `cases.documents_content` | Text is keyed by document id now, not opinion id | | `opinions.opinion_relations` | `cases.document_relations` ⋈ `cases.treatments` | One table becomes two. Authority is on the edge, treatment text on the child | | `opinion_relations.citator_version` | — | Removed; it described our internal analysis run | | — | every non-opinion docket entry, plus document metadata | New: roughly 17 million additional entries | | — | `per_curiam`, `en_banc`, `precedential_note`, `title`, `pincite`, `citation_type`, `date_terminated`, `country`, `in_use`, `pacer_court_id`, `freelaw_court_id` | New columns | **Opinion IDs are unchanged.** `cases.opinions.id` holds exactly the value `opinions.docket_entries.id` held, so any mapping you keep keyed on opinion id survives the move untouched. Document, citation and relation IDs are new. ### The three real edits **1. `is_opinion` → `opinion_id IS NOT NULL`.** ```sql -- before SELECT * FROM opinions.docket_entries WHERE is_opinion = true; -- after SELECT * FROM cases.docket_entries WHERE opinion_id IS NOT NULL; ``` **2. Opinion text now joins through `documents`.** ```sql -- before SELECT de.id, c.html_content FROM opinions.docket_entries de JOIN opinions.content c ON c.id = de.id WHERE de.is_opinion = true; -- after SELECT de.opinion_id, dc.html_content FROM cases.docket_entries de JOIN cases.documents d ON d.docket_entry_id = de.id JOIN cases.documents_content dc ON dc.id = d.id WHERE de.opinion_id IS NOT NULL AND de.deleted_at IS NULL; ``` **3. Treatments join through the edge.** ```sql -- before SELECT citing_id, cited_id, treatment_category, is_authoritative FROM opinions.opinion_relations; -- after SELECT r.citing_id, r.cited_id, t.treatment_category, r.is_authoritative FROM cases.document_relations r LEFT JOIN cases.treatments t ON t.relation_id = r.id; ``` Use `LEFT JOIN`: an edge can exist without a treatment. ### Two things that are not carried over **Citation counts.** The old surface exposed a precomputed `citation_counts` view. It is not part of `cases`; compute it directly: ```sql SELECT cited_id, count(*) AS citation_count FROM cases.document_relations WHERE deleted_at IS NULL GROUP BY cited_id; ``` If that aggregate is on a hot path for you, tell us — we would rather know than have you discover the cost in production. **`_active` views.** There are none, in either schema. Filter `WHERE deleted_at IS NULL` yourself, as you do today. --- ## Laws Schema The `laws` schema contains statutes (codified laws), regulations (administrative rules), and constitutions. This schema uses a unified document structure with hierarchical support and versioning. ### Entity Relationship ``` collections (uscode, cfr, state codes) └── documents (hierarchical: title > chapter > section) └── version_relations (lifecycle edges between document versions) ``` ## Laws Tables ### laws.collections Content collections (e.g., U.S. Code, CFR, state statutes and regulations). | Column | Type | Description | |--------|------|-------------| | `id` 🔑 | VARCHAR(50) | Collection identifier (e.g., 'uscode', 'cfr') | | `name` | VARCHAR(200) | Full name (e.g., 'United States Code') | | `type` | VARCHAR(30) | Content type: statute, regulation, constitution, guidance, executive_order, or notice | | `authority_name` | VARCHAR(200) | Publishing authority (e.g., 'United States', 'California') | | `state` | VARCHAR(50) | Jurisdiction name (e.g., 'Federal', 'California', 'New York') | | `country` | VARCHAR(2) | ISO 3166-1 alpha-2 country code | | `created_at`, `updated_at` | TIMESTAMPTZ | Record timestamps | | `deleted_at` | TIMESTAMPTZ | Soft-deletion timestamp | Use `GET /api/v1/laws/collections` for the current machine-readable list, or the [live coverage overview](https://app.midpage.ai/coverage/us) for a human-readable view. --- ### laws.documents Unified table for hierarchical legal content (statutes, regulations, constitutions). | Column | Type | Description | |--------|------|-------------| | `id` 🔑 | UUID | Document identifier | | `collection_id` → | VARCHAR(50) | Parent collection (e.g., 'uscode', 'cfr') | | `parent_id` → | UUID | Parent document node | | `path` | LTREE | Hierarchical path for tree queries (e.g., 'uscode.t5.ch1.s101') | | `sort_order` | INTEGER | Order within parent | | `node_type` | VARCHAR(20) | Structural type: `PARENT`, `LEAF`, or `TOMBSTONE` | | `number` | VARCHAR(50) | Section/chapter number (e.g., '5', 'I', '101', '(a)') | | `heading` | TEXT | Section heading/title | | `citation_canonical` | VARCHAR(200) | Canonical citation (e.g., '5 U.S.C. § 552', '26 C.F.R. § 1.61-1') | | `effective_date` | DATE | When this version became effective | | `publication_date` | DATE | Source-stated publication date | | `source_as_of` | DATE | The source's own as-of or edition date | | `is_historical` | HISTORICAL_STATUS | `YES` when the source marks this version superseded; otherwise `SCRAPER_DIDNT_SAY` | | `html_content` | TEXT | HTML formatted content | | `content_hash` | VARCHAR(64) | Hash used to detect content changes | | `source_law` | VARCHAR(500) | Public law reference for statutes (e.g., 'Pub. L. 118-100') | | `agency_name` | VARCHAR(200) | Agency name for regulations (e.g., 'Environmental Protection Agency') | | `agency_code` | VARCHAR(100) | Agency code for regulations (e.g., 'EPA') | | `agencies` | JSONB | Structured agency metadata, when supplied by the source | | `register_citation` | TEXT | Federal or state register citation (e.g., '89 FR 12345') | | `source_id` | VARCHAR(200) | External source identifier | | `source_url` | TEXT | Official source URL | | `gcs_path` | VARCHAR(500) | GCS path to original source file | | `first_scraped_at`, `last_scraped_at` | TIMESTAMPTZ | Observation timestamps | | `created_at`, `updated_at` | TIMESTAMPTZ | Record timestamps | | `deleted_at` | TIMESTAMPTZ | Soft-deletion timestamp | Currency is derived at query time from the document and its lifecycle edges; there is no stored `is_current`, `status`, `end_date`, or `supersedes_id` column. --- ### laws.version_relations Lifecycle and lineage edges between document versions. Edges point from the newer or terminal-marker row (`from_id`) to its predecessor (`to_id`). Dates remain on the document rows rather than the edge. | Column | Type | Description | |--------|------|-------------| | `id` 🔑 | UUID | Relation identifier | | `from_id` → | UUID | Newer version or lifecycle marker | | `to_id` → | UUID | Predecessor version | | `relation_type` | VARCHAR(30) | `succeeds`, `transferred`, `removed`, or `repeals` | | `created_at` | TIMESTAMPTZ | Edge creation timestamp | 🔑 = Primary Key, → = Foreign Key # Midpage HTML Format This document describes Midpage's semantic HTML format for legal opinions. Approximately 62% of the corpus uses this format, 37% uses Harvard Format, and only about 200,000 opinions—roughly 1%—remain in older formats. See [Format Variations](format-variations.md) for examples of each format. ## Case Information Case metadata is wrapped in a `
` block at the top of the document: ```html
Case Information

DOE v. ACME CORP.

No. 23-1456

United States Court of Appeals, Ninth Circuit

March 15, 2024

Rehearing Denied May 1, 2024

95 F.4th 123

SMITH, Circuit Judge

``` **Attributes:** - `shortName`: Bluebook-style short case name - `docket`: Docket number without "No." prefix - `court`: Bluebook court abbreviation - `date`: `YYYY-MM-DD` format --- ## Page Numbers Page breaks are marked with self-closing `` tags: ```html

Text on page 123.

Text on page 124.

``` Page breaks can occur mid-paragraph: ```html

Text starts on page 123 and continues on page 124.

``` The page numbers follow a single reporter, which is not necessarily the first entry in the opinion's `citations`. `/opinions/get` returns that reporter's citation as `pagination_citation`; pair the page numbers only with that citation. When it is `null`, the reporter could not be established and pinpoint pages should be omitted. --- ## Footnotes Footnote marks link to footnote content collected in a section at the end of the document. Each footnote includes a link back to its inline mark. ```html

Text with a footnote1 reference.

...

Footnotes

1 The footnote content.
``` --- ## Section Headings Standard HTML heading tags are used: ```html

MEMORANDUM AND ORDER

I. BACKGROUND

A. Procedural History

``` --- ## Blockquotes Quoted material uses `
` with inner `

` tags: ```html

All disputes shall be resolved by binding arbitration.

``` --- ## Signature Block ```html

WILLIAM H. SMITH

UNITED STATES CIRCUIT JUDGE

``` --- ## Case Citations Case citations are wrapped in `` tags: ```html Smith v. Jones, 262 F.3d 305, 320 (4th Cir. 2001) ``` **Attributes:** | Attribute | Description | |-----------|-------------| | `case` | Case name (parties) | | `cite` | Volume + reporter + starting page (e.g., `262 F.3d 305`) | | `pinpoint` | Specific page(s) cited (e.g., `320`, `320-22`) | | `court` | Bluebook court abbreviation | | `date` | Year or full date (`YYYY` or `YYYY-MM-DD`) | | `docket` | Docket number (optional) | | `parallel` | Parallel citations, pipe-separated | | `type` | `short`, `id`, or `supra` for non-full citations | **Short-form and Id. citations:** ```html Smith, 262 F.3d at 325 Id. at 326 ``` --- ## Law Citations Statutes, regulations, rules, and constitutional provisions are wrapped in `` tags: ```html 18 U.S.C. § 3582(c)(1)(A) Fed. R. Civ. P. 56(a) Gov. Code, § 9606 ``` **Attributes:** | Attribute | Description | |-----------|-------------| | `jurisdiction` | `US` for federal, two-letter state code for state | | `cite` | Normalized citation string | | `type` | `short` or `id` for non-full citations | --- ## Example Document ```html
Case Information

DOE v. ACME CORP.

No. 23-1456

United States Court of Appeals, Ninth Circuit

March 15, 2024

95 F.4th 123

MEMORANDUM AND ORDER

SMITH, Circuit Judge:

Plaintiff appeals the district court's1 grant of summary judgment under Fed. R. Civ. P. 56(a). The Supreme Court emphasized that arbitration is "a matter of consent, not coercion." Volt Info. Scis., Inc. v. Bd. of Trs., 489 U.S. 468, 479 (1989).

The contract stated:

All disputes shall be resolved by binding arbitration.

For these reasons, we AFFIRM.

WILLIAM H. SMITH

UNITED STATES CIRCUIT JUDGE

Footnotes

1 The Honorable Jane Williams, presiding.
``` # HTML Format Variations The [**HTML Content Format**](content-format.md) page describes the standard Midpage Format. Opinion content is currently divided into three formats: - **62% — Midpage Format** - **37% — Harvard Format** - **1% — Legacy formats** The examples below show the markup used by each format. Midpage examples are expanded by default; expand Harvard Format or Legacy to compare formats. --- ## Case Header Structured `
` with data attributes. ```html
Case Information

DOE v. ACME CORP.

No. 23-1456

United States Court of Appeals, Ninth Circuit

March 15, 2024

``` Uses an `` element rather than a Midpage case-information block. ```html MARKUS, J.

Opinion text...

```
Some of the remaining older opinions use an unstructured `
` block with raw styled text from the source document. ```html
Case Information

No. 05-13-01080-CV BRETT SHIPP, Appellant V. DR. RICHARD MALOUF, Appellees

``` --- ## Page Numbers Custom `` element. ```html ``` XML `` element. ```html *303 ``` Some older PDF/OCR content uses a ``. ```html *124 ``` Some legacy HTML has no page markers. --- ## Footnotes Custom elements with bidirectional anchor links. ```html 1 ...
1 The footnote text...
```
Anchor-based marks and footnote containers. ```html 1 ...

Notes

1

The footnote text...

```
Older Midpage content may use simpler self-closing marks with no anchor links. ```html ... The footnote text... ``` Older PDF/OCR content may use class-based `` and `

` elements. ```html [1] ...

[1] The footnote text...

```
--- ## Blockquotes Standard `
` element. ```html

All disputes shall be resolved by binding arbitration.

``` Also uses structural `
` elements, often with an identifier from the source document. ```html

Quoted material...

``` Some legacy HTML uses indentation or `
` blocks instead.



---

## Case Citations



Structured `` with metadata attributes.

```html

  Smith v. Jones, 262 F.3d 305, 320 (4th Cir. 2001)

```





Citations enriched by Eyecite use `` elements
with source-specific metadata.

```html
500 Pa. 465
```





Some older PDF/OCR opinions have no citation markup.

```html
Ashcroft v. Iqbal, 556 U.S. 662, 678 (2009)
```



# Citator

Understand how cases have been treated by subsequent courts.

The Midpage citator analyzes how opinions cite and treat other opinions, providing signals about whether a case remains good law and how it has been interpreted over time.

## Authoritative Citations

The citator is run on **authoritative citations only**. A citation is considered authoritative when it comes from:

- **Higher courts** — Appellate courts citing district court decisions, supreme courts citing appellate decisions
- **Same court** — A court citing its own prior decisions

This focus on authoritative citations ensures the treatment signals are legally meaningful. Citations from lower courts or courts in different jurisdictions are tracked in the `opinion_relations` table but do not receive treatment analysis.

## Treatment Data

Each citator entry includes:

| Field | Description |
|-------|-------------|
| `treatment_category` | High-level classification: "Negative", "Caution", or "Neutral" |
| `treatment_description` | Detailed explanation of how the citing opinion treats the cited opinion |
| `is_authoritative` | Boolean indicating if this is an authoritative citation |
| `supporting_quote` | Excerpt from the citing opinion supporting the treatment classification |
| `citing_id` | Opinion ID of the case doing the citing |

## Accessing Citator Data

### REST API

Include `include_detailed_treatments: true` in your `/opinions/get` request:

```json
{
  "opinion_ids": ["145875"],
  "include_detailed_treatments": true
}
```

Response includes a `treatments` array for each opinion:

```json
{
  "opinions": [{
    "id": "145875",
    "case_name": "Ashcroft v. Iqbal",
    "treatments": [
      {
        "citing_id": "8623588",
        "treatment_category": "Neutral",
        "treatment_description": "Followed and applied the plausibility standard",
        "is_authoritative": true,
        "supporting_quote": "Under Iqbal, a complaint must contain sufficient factual matter..."
      }
    ]
  }]
}
```

### SQL

Query the `opinion_relations` table directly:

```sql
SELECT 
  citing_id,
  treatment_category,
  treatment_description,
  supporting_quote
FROM opinions.opinion_relations
WHERE cited_id = '145875'
  AND is_authoritative = true
ORDER BY citing_id DESC;
```

## Use Cases

- **Case validation** — Check if a case has been overruled or limited
- **Research** — Find cases that follow or distinguish a key precedent
- **Citation analysis** — Understand how courts have interpreted specific holdings