---
title: "Read a lease (PDF, JPEG or PNG, at most 4 MB and, for a PDF, 40 pages, as base64) with an AI service (Claude Haiku 4.5 through OpenRouter) and answer the facts the new-case form takes. For a file the free reading on the device could not read. With `kind: \"correspondence\"` and `files` (1 to 10 messages with the tenant: PDF, JPEG, PNG, plain text `text/plain`, or a saved email `message/rfc822` (`.eml`, at most 1 MB, turned into its text before it is sent); 8 MB, 40 PDF pages and 200,000 characters of text in all), it drafts what happened for a lease-violation case (`violationDescription`, `violationDate`): a draft for the person to read and correct, never a checked answer. One request counts once against the hourly limit, whatever the number of files. With `kind: \"ledger\"` and one `file` (a tenant ledger PDF, at most 4 MB and 40 pages), it answers the ledger's rows (`rows`, each `{ date, type: \"charge\" | \"payment\", amountCents, memo }`, at most 500, in the order printed) a balance brought forward from before them (`openingBalanceCents`, null when none) and the ending balance the ledger states (`endingBalanceCents`, null when it states none), for the caller to check: the balance brought forward and the rows should add up to the ending balance, and the website fills in nothing when they do not. A row dated after today is left out. The files are sent to OpenRouter only for this one reading, under fixed names (the `name` you send is never passed on), on endpoints that keep no data; nothing is stored and nothing of the files or the answer is logged. Every answer is a suggestion for a person to check: a fact the reading is unsure of is left out, never guessed. When the service refuses, fails or takes over 30 seconds, the answer is empty (`{ \"suggestions\": {}, \"confidence\": {} }`, or for a ledger `{ \"rows\": [], \"openingBalanceCents\": null, \"endingBalanceCents\": null }`), never an error. For now only the signed-in app and test keys can use it: a live API key answers 503 `extract_unavailable`. At most 20 an hour per organization (429 `rate_limited`); 503 `extract_unavailable` when the server has no AI service set"
method: POST
path: "/v1/extract"
---

# Read a lease (PDF, JPEG or PNG, at most 4 MB and, for a PDF, 40 pages, as base64) with an AI service (Claude Haiku 4.5 through OpenRouter) and answer the facts the new-case form takes. For a file the free reading on the device could not read. With \`kind: "correspondence"\` and \`files\` (1 to 10 messages with the tenant: PDF, JPEG, PNG, plain text \`text/plain\`, or a saved email \`message/rfc822\` (\`.eml\`, at most 1 MB, turned into its text before it is sent); 8 MB, 40 PDF pages and 200,000 characters of text in all), it drafts what happened for a lease-violation case (\`violationDescription\`, \`violationDate\`): a draft for the person to read and correct, never a checked answer. One request counts once against the hourly limit, whatever the number of files. With \`kind: "ledger"\` and one \`file\` (a tenant ledger PDF, at most 4 MB and 40 pages), it answers the ledger's rows (\`rows\`, each \`{ date, type: "charge" | "payment", amountCents, memo }\`, at most 500, in the order printed) a balance brought forward from before them (\`openingBalanceCents\`, null when none) and the ending balance the ledger states (\`endingBalanceCents\`, null when it states none), for the caller to check: the balance brought forward and the rows should add up to the ending balance, and the website fills in nothing when they do not. A row dated after today is left out. The files are sent to OpenRouter only for this one reading, under fixed names (the \`name\` you send is never passed on), on endpoints that keep no data; nothing is stored and nothing of the files or the answer is logged. Every answer is a suggestion for a person to check: a fact the reading is unsure of is left out, never guessed. When the service refuses, fails or takes over 30 seconds, the answer is empty (\`{ "suggestions": {}, "confidence": {} }\`, or for a ledger \`{ "rows": \[\], "openingBalanceCents": null, "endingBalanceCents": null }\`), never an error. For now only the signed-in app and test keys can use it: a live API key answers 503 \`extract_unavailable\`. At most 20 an hour per organization (429 \`rate_limited\`); 503 \`extract_unavailable\` when the server has no AI service set

`POST /v1/extract`

## Request

- Base URL: `https://api.evictionsapi.com`
- URL: `https://api.evictionsapi.com/v1/extract`
- Auth: one of:
  - HTTP bearer
  - HTTP bearer

## Request body

- union
  - object
    - `kind` 'lease', required
    - `file` object, required
      - `name` string, required
      - `contentType` string, required
      - `contentBase64` string, required
  - object
    - `kind` 'correspondence', required
    - `files` object[], required
      - `name` string, required
      - `contentType` string, required
      - `contentBase64` string, required
  - object
    - `kind` 'ledger', required
    - `file` object, required
      - `name` string, required
      - `contentType` string, required
      - `contentBase64` string, required

## Response `200`

The answers found, in the same shape as the website's lease reading: each a `{ value, source: "lease_file" }` (`source: "messages"` for the messages' draft), and how sure the reading is of each (`high` or `medium`). Dates are `YYYY-MM-DD`, `monthlyRent` is dollars with cents ("1450.00"), `noticePeriod` is whole days ("3"), `state` is the two-letter code; `parties` lists the landlord (`plaintiff`) and the tenants (`defendant`). From messages: `violationDescription` (a few plain sentences, at most 2,000 characters) and `violationDate` (never later than today).

- union
  - object
    - `suggestions` object, required
      - `line1` object
        - `value` string, required
        - `source` 'lease_file', required
      - `unit` object
        - `value` string, required
        - `source` 'lease_file', required
      - `city` object
        - `value` string, required
        - `source` 'lease_file', required
      - `state` object
        - `value` string, required
        - `source` 'lease_file', required
      - `zip` object
        - `value` string, required
        - `source` 'lease_file', required
      - `ownerOfRecord` object
        - `value` string, required
        - `source` 'lease_file', required
      - `leaseStart` object
        - `value` string, required
        - `source` 'lease_file', required
      - `leaseEnd` object
        - `value` string, required
        - `source` 'lease_file', required
      - `monthlyRent` object
        - `value` string, required
        - `source` 'lease_file', required
      - `noticePeriod` object
        - `value` string, required
        - `source` 'lease_file', required
      - `parties` object
        - `value` object[], required
          - `role` 'plaintiff' | 'defendant', required
          - `name` string, required
          - `isEntity` boolean, required
        - `source` 'lease_file', required
      - `violationDescription` object
        - `value` string, required
        - `source` 'messages', required
      - `violationDate` object
        - `value` string, date, required
        - `source` 'messages', required
    - `confidence` object, required
  - object: For `kind: "ledger"`.
    - `rows` object[], required
      - `date` string, date, required
      - `type` 'charge' | 'payment', required
      - `amountCents` integer, required: Whole cents, always positive; `type` says which way it moves the balance.
      - `memo` string, required
    - `openingBalanceCents` integer, nullable, required: A balance brought forward from before the first row, in whole cents (negative for a credit); null when the ledger shows none. It is not among `rows`.
    - `endingBalanceCents` integer, nullable, required: The balance the ledger states at its end, in whole cents (negative for a credit); null when it states none. `openingBalanceCents` plus the rows should come to it.

## Other responses

- `400`: The request is invalid (`invalid_request`): `kind` must be `lease` or `ledger` with one `file`, or `correspondence` with `files` (1 to 10), and each file must have exactly `name`, `contentType` and `contentBase64` (valid base64).
- `401`: The credential is missing or invalid.
- `403`: The credential is not allowed to do this (`forbidden`). For a signed-in session: `permission_denied`, the person's role in the organization lacks the permission this needs (ask an owner); `no_organization`, the person belongs to no organization yet (start one with `POST /v1/me/organizations` or accept an invitation).
- `404`: Not found.
- `409`: The request conflicts with the current state: for a case, it is not in a state that allows this, or its details were erased (`erased`); for an API key, a limit or the key in use.
- `413`: `payload_too_large`: a file is over 4 MB, a PDF has more than 40 pages, a text or `.eml` message is over 1 MB, or the messages together are over 8 MB, 40 PDF pages or 200,000 characters of text (the body itself may be at most 14 MB).
- `415`: `unsupported_media_type`: a file is not a PDF, JPEG or PNG (or, for a message, plain UTF-8 text or a `.eml` email; for a ledger, only a PDF), `contentType` is not the type of its bytes, a `.eml` file is not an email, or a PDF's pages could not be counted.
- `422`: The case did not pass a check.
- `429`: `rate_limited`: the organization has made 20 requests this hour (the server may set another number). Every request counts, a refused one too: the limit is checked before any file is read. `Retry-After` gives the seconds to wait.
- `503`: `extract_unavailable`: reading files with the AI service is not set up on this server, or is not open to live API keys yet (only the signed-in app and test keys can use it for now). Fill in the form by hand, or read the file on the device.

---

[API](https://skmtc.dev/evictionsapi/apis/evictions-api.md) · [All operations](https://skmtc.dev/evictionsapi/apis/evictions-api/llms.txt) · [OpenAPI document](https://skmtc.dev/evictionsapi/apis/evictions-api/revisions/28459213a1a3?raw)
