# Ezel API Reference Ezel is an AI legal platform. This document is the complete reference for the Ezel REST API and MCP server, written as plain markdown for LLMs and agents. - **Base URL:** `https://app.ezel.ai/api/v1` - **Authentication:** send a personal API key as `Authorization: Bearer ezel_live_...` (the header `X-API-Key` is also accepted). - **Content type:** all responses are JSON unless streaming (see Ask). Request bodies are JSON unless noted (file upload is multipart/form-data). - **OpenAPI:** a machine-readable spec is at `https://app.ezel.ai/api/v1/openapi.json`. The API is organised around **matters** (cases or projects) and the **documents** inside them. Anything a user can do in the Ezel app, they can do with the API, gated by the same plan and permissions on their account. --- ## Authentication Every request needs a personal API key, sent as a bearer token: ``` Authorization: Bearer ezel_live_... ``` Keys are created and revoked in the Ezel app under **Settings > Connector**. The full key is shown once at creation; only a hash is stored. The same key works for the REST API and the MCP server. ## Scopes Each key carries one or more scopes: - `read` — every query endpoint (whoami, usage, listing/reading matters and files, ask, search, case law, reading review tables). - `write` — mutations (create/delete a matter, upload/delete a file, create/run a review table). A request that needs a scope the key lacks returns `403` with `type` `insufficient_scope`. ## Rate limits Per key, per category, over a rolling 60-second window. Every response carries `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset` headers; exceeding a limit returns `429` with `Retry-After`. - Ask (the `/ask` endpoints): 20/min - Search (document + case law): 60/min - Write: 60/min - Default (reads): 150/min Plan-based token usage limits are enforced separately and surface as `429` with `type` `usage_limit`. ## Errors Errors use standard HTTP status codes and a consistent body: ```json { "error": { "type": "not_found", "message": "Matter not found." } } ``` Common types: `unauthorized` (401), `forbidden` / `insufficient_scope` (403), `not_found` (404), `bad_request` (400), `usage_limit` / `rate_limited` (429). --- ## GET /whoami Verify a key and return the account behind it. ```bash curl https://app.ezel.ai/api/v1/whoami -H "Authorization: Bearer ezel_live_..." ``` Response: ```json { "user_id": "5d6bbf5e-...", "full_name": "Alex Morgan", "plan": "Pro", "scopes": ["read", "write"], "enabled_features": ["matters", "chat", "caselaw"] } ``` ## GET /usage Storage usage and current message-limit status. Scope: `read`. ```json { "plan": "Pro", "storage": { "used_bytes": 13250667, "limit_bytes": 53687091200, "used_percent": 0.02 }, "messages": { "within_limits": true, "status": "...", "reset_at": null } } ``` ## GET /matters List the user's matters, pinned first, each with a document count. Scope: `read`. ```json { "matters": [ { "id": "2a951d7c-...", "name": "Acme acquisition", "client_name": "Acme Corp", "document_count": 6 } ], "total": 1 } ``` ## POST /matters Create a matter. The name must be unique within the account. Scope: `write`. Body: `name` (string, required), `client_name` (string), `description` (string), `pinned` (boolean). ```bash curl https://app.ezel.ai/api/v1/matters \ -H "Authorization: Bearer ezel_live_..." \ -H "Content-Type: application/json" \ -d '{"name":"Acme acquisition","client_name":"Acme Corp"}' ``` Returns `201` with `{ "matter": { "id": "...", "name": "...", "document_count": 0 } }`. ## GET /matters/{matter_id} Get one matter with its document count. Scope: `read`. ## DELETE /matters/{matter_id} Delete a matter. Its files and chats are not deleted; they are released back to the drive (their `matter_id` is cleared). Scope: `write`. ```json { "deleted": true, "released_documents": 6, "released_chats": 2 } ``` ## GET /matters/{matter_id}/files List the documents in a matter, newest first. Scope: `read`. ## POST /matters/{matter_id}/files Upload a document as `multipart/form-data`. The file is stored, its text indexed for search, and scanned PDFs are OCR'd in the background so they become searchable and answerable. Max 20 MB per file. Scope: `write`. Form field: `file` (binary, required). ```bash curl https://app.ezel.ai/api/v1/matters/MATTER_ID/files \ -H "Authorization: Bearer ezel_live_..." \ -F "file=@contract.pdf" ``` Returns `201` with `{ "file": { "id": "...", "filename": "contract.pdf", "file_size": 109223 } }`. ## DELETE /matters/{matter_id}/files/{file_id} Remove a file from a matter and delete it from storage. Scope: `write`. ## POST /ask Ask a general U.S. legal question. Research mode is on, so the agent can reach real case law on its own. Not scoped to the user's documents (use the matter ask for that). Scope: `read`. Body: `question` (string, required), `stream` (boolean). ```bash curl https://app.ezel.ai/api/v1/ask \ -H "Authorization: Bearer ezel_live_..." \ -H "Content-Type: application/json" \ -d '{"question":"What is the holding of Gideon v. Wainwright?"}' ``` Response: ```json { "answer": "Gideon v. Wainwright held that...", "citations": [], "sources": [], "conversation_id": "87a96194-..." } ``` When `stream` is `true`, the response is `text/event-stream`: `data: {"type":"delta","text":...}` frames as the answer is written, then a final `data: {"type":"done","answer":...,"citations":[...],"sources":[...],"conversation_id":...}`, then `data: [DONE]`. ## POST /matters/{matter_id}/ask Ask a question answered only from the documents in a specific matter, with citations back to them. Same `stream` flag as `/ask`. Scope: `read`. Body: `question` (string, required), `stream` (boolean). ## POST /matters/{matter_id}/search Keyword or regex search across a matter's documents, returning page-level snippets. Use this for exact matches rather than an AI answer. Scope: `read`. Body: `terms` (string or string array, OR by default), `regex` (string, POSIX, case-insensitive), `match_all` (boolean, AND), `count_only` (boolean), `max_results` (integer). ```bash curl https://app.ezel.ai/api/v1/matters/MATTER_ID/search \ -H "Authorization: Bearer ezel_live_..." \ -H "Content-Type: application/json" \ -d '{"terms":["indemnify","indemnification"],"match_all":false}' ``` Response (snippets): ```json { "results": [ { "document_id": "3ff8...", "filename": "contract.pdf", "page": 4, "snippet": "...shall indemnify and hold harmless..." } ] } ``` With `count_only: true`, returns `{ "documents": [{ "document_id", "filename", "matches" }], "matching_documents": N }`. ## GET /caselaw/search Search real, citable U.S. case law (via CourtListener), ranked by relevance. Scope: `read`. Query params: `query` (string, required), `jurisdiction` (string), `year_start` (integer), `year_end` (integer), `published_only` (boolean). ```bash curl "https://app.ezel.ai/api/v1/caselaw/search?query=right+to+counsel" \ -H "Authorization: Bearer ezel_live_..." ``` Response: ```json { "cases": [ { "case_name": "Gideon v. Wainwright", "court": "Supreme Court of the United States", "date_filed": "1963-03-18", "citation": "372 U.S. 335", "cluster_id": 8954562 } ], "count": 7 } ``` Pass a `cluster_id` to `GET /cases/{cluster_id}` for the full opinion. ## GET /cases/{cluster_id} Fetch the full opinion text and metadata for a case. Long opinions are truncated with `truncated: true`. Scope: `read`. ```json { "case_name": "Rhode Island v. Innis", "court": "Supreme Court of the United States", "citation": "446 U.S. 291", "plain_text": "Mr. Justice Stewart delivered...", "truncated": false } ``` ## POST /matters/{matter_id}/review-tables Create a review table: a grid of documents (rows) by questions (columns), where each cell is an AI-extracted answer with a cited quote and page. Define columns and documents in one call; the table starts filling immediately unless `run` is `false`. Poll `GET /review-tables/{table_id}` for cell status. Scope: `write`. Body: - `columns` (array, required): each `{ "header", "prompt", "type" }`. The `prompt` is the question asked of every document. `type` is one of `text`, `number`, `date`, `boolean`. - `all_documents` (boolean): add every document in the matter as a row. - `document_ids` (array of uuid): or add specific documents. - `name` (string): optional table name. - `run` (boolean, default true). ```bash curl https://app.ezel.ai/api/v1/matters/MATTER_ID/review-tables \ -H "Authorization: Bearer ezel_live_..." \ -H "Content-Type: application/json" \ -d '{ "name": "Key terms", "columns": [ {"header":"Parties","prompt":"Who are the parties?","type":"text"}, {"header":"Date","prompt":"Effective date?","type":"date"} ], "all_documents": true }' ``` ## GET /review-tables/{table_id} Return the table with its columns, rows, and cells. Each cell has a `status` (`pending`, `running`, `done`, `not_found`, or `error`) and, when done, the extracted value plus a cited quote and page. Poll this while a table fills. Scope: `read`. ```json { "table": { "id": "bd85...", "status": "ready" }, "columns": [{ "header": "Parties" }], "rows": [{ "filename": "contract.pdf" }], "cells": [{ "status": "done", "value": "Acme Corp; Beta LLC" }] } ``` ## POST /review-tables/{table_id}/run Fill any pending cells and retry errored ones. Cells normally fill automatically as columns and documents are added, so this is mostly for retries. Scope: `write`. --- ## MCP (Model Context Protocol) Ezel is also a remote MCP server, so it can be used directly from Claude (claude.ai and Desktop), Cursor, Claude Code, and ChatGPT. It exposes a read-only slice of this API as MCP tools. - **Server URL:** `https://app.ezel.ai/mcp` - **Claude** connects via OAuth: add a custom connector with the Server URL, sign in, and click Allow. No API key needed. - **Cursor / Claude Code / others** use an API key as a bearer token (the same `ezel_live_` key as the REST API). Claude Code example: ```bash claude mcp add ezel --transport http https://app.ezel.ai/mcp \ --header "Authorization: Bearer ezel_live_..." ``` Tools: - `ezel_list_matters` — list the user's matters. - `ezel_ask_matter` — cited answer from a matter's documents. - `ezel_ask` — general legal Q&A with research. - `ezel_search_caselaw` — ranked U.S. case law search. - `ezel_get_case` — full opinion text for a case. Access and limits are enforced identically to the REST API: a tool the user's plan does not allow does not appear in `tools/list` and cannot be called, and hitting a usage limit blocks the call.