# API reference

> The conventions of the Evictions API REST API, an index of every endpoint, the error codes and the rate limits.

Source: https://evictionsapi.com/docs/api

The API is at https://api.evictionsapi.com and every path starts with `/v1`. The same reference as an OpenAPI 3.1 document: https://evictionsapi.com/openapi.json

## Conventions

- Send `Authorization: Bearer <key>` on every request except the public endpoints. A key has a mode, which is in its prefix: `eak_live_...` for live and `eak_test_...` for test. A key sees only the cases of its own organization and mode.
- Test keys are free. They are a developer feature for building and testing an integration: a test key works only with the fictional test state `ZZ`, and nothing real is served or filed. Create a test key on the site under "API keys", or with `POST /v1/api-keys`.
- Live mode is for real cases, for a rental property in any of the 50 states or DC. Submitting a live case serves nothing and files nothing: a licensed attorney in the property’s state is engaged for that case after it is submitted, and the case moves when that attorney acts. A live key can be created only by an owner of the organization, signed in on the site, under "API keys".
- Every case response has `state` and `nextAction` (`by` and `description`), which say who the case is waiting on and for what.
- Every error has the shape `{ "error": { "code", "message", "details" } }` and an HTTP status. Branch on `code`, which is stable. An unknown route is 404 `not_found`.
- A POST that needs a credential accepts an `Idempotency-Key` header of 1 to 255 characters; a repeat of the same request returns the stored response with `Idempotent-Replayed: true`.

## Endpoints

Each group has its own page, with the parameters, an example request and the responses of every endpoint.

### Customer endpoints

Called with an organization API key (or, from the website, a signed-in session). Everything you need to create, check, submit and follow a case. Full reference: https://evictionsapi.com/docs/api/customer.md

- `GET /v1/jurisdictions/resolve`: Whether a jurisdiction is served, and its notice rules
- `POST /v1/geo/county`: Find the county of a street address, from the US Census geocoder. The address goes in the body, never the URL; the unit is not passed on, and nothing is stored. When the county cannot be found (no match, a match in another state or in more than one county, or the geocoder failing or taking over 5 seconds) the answer is `{ "county": null }`, never an error. At most 60 an hour per organization (429 `rate_limited`)
- `GET /v1/intake/address`: The organization's email intake address, `cases+<token>@<domain>`: email sent to it by a member of the organization whose address is confirmed puts its attachments and its text in the intake inbox. Made on the first call, the same afterwards. Live only: a test key answers 422 `live_only`
- `POST /v1/intake/address/rotate`: Replace the organization's email intake address with a new one. Email sent to the old address is no longer received. A signed-in owner only (an API key answers 403). Live only
- `GET /v1/integrations`: List the organization's stored property-management connections (Buildium, DoorLoop), without their credentials. A signed-in session in live mode only: an API key answers 403
- `POST /v1/integrations/{system}/connect`: Connect Buildium (`credentials: { clientId, clientSecret }`) or DoorLoop (`credentials: { token }`) for the organization. The key is checked with one read-only call to the system, then stored encrypted, replacing any earlier one; it is never returned. An owner's signed-in session in live mode only
- `DELETE /v1/integrations/{system}`: Disconnect a property-management system: its stored key is deleted. An owner's signed-in session in live mode only
- `POST /v1/integrations/{system}/leases`: Search the connected system's leases: by tenant name in Buildium, by lease name (which names its unit and tenants) in DoorLoop. At most 20 leases. A signed-in session in live mode only
- `POST /v1/integrations/{system}/pull`: Read one lease from the connected system as suggested answers for a new case: the address, the tenants, the lease dates and rent, the ledger and the system's balance, and its lease PDF when it has one. A part the system no longer has (a tenant, the unit) is left out. Nothing is stored or filed by this. A signed-in session in live mode only
- `GET /v1/intake/files`: List the files waiting in the organization's intake inbox, oldest first: files the customer already has (a lease, a ledger, an email) kept until a case is started. Live only: a test key answers 422 `live_only`
- `POST /v1/intake/files`: Keep a file in the organization's intake inbox until a case is started: a PDF, JPEG or PNG of at most 4 MB, decided by its contents. The inbox holds 50 waiting files (409 `inbox_full`), each for 30 days. Live only: a test key answers 422 `live_only`
- `POST /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
- `GET /v1/intake/files/{id}`: Download a waiting file of the intake inbox. A file moved onto a case is downloaded as that case's document instead (404 here). Live only
- `DELETE /v1/intake/files/{id}`: Delete a waiting file of the intake inbox. Live only
- `POST /v1/cases`: Create a draft case. For a live case the property must be in a US state or DC (422 `unsupported_state`) and the case is an attorney-direct case. `service: "full"` asks for the full-service path (we act as your agent, no attorney reviews the case); the case gets it only where that path is open for the property, and otherwise answers 409 `service_unavailable` without creating a different service: check availability first
- `GET /v1/cases`: List the cases of the key's organization and mode, optionally only those one person opened (`openedBy`), or those of one landlord (`landlordId`, or `none` for the cases with no landlord)
- `GET /v1/cases/{id}`: Get a case
- `PATCH /v1/cases/{id}`: Change the facts of a draft case, or (`paymentMethodId`, in any state) the saved card a full-service case is charged to. 409 `not_agent_filed` for a card on a case that is not full-service; 422 `payment_method_not_found` for a card that is not one of the organization's usable cards in this mode
- `GET /v1/cases/{id}/managed-facts`: Which facts one proposed act of a managed case is missing. The facts are the customer's declaration, and an empty list is not permission to perform the act
- `POST /v1/cases/{id}/documents`: Upload evidence for a draft case: a PDF, JPEG or PNG, decided by the file's contents (415 `unsupported_media_type` otherwise, for a live case). A `correspondence` document may also be plain UTF-8 text, such as the body of a forwarded email, stored as `text/plain`. For a live case, `purpose: "other"` files can also be added after submission, until the case is finished
- `POST /v1/cases/{id}/documents/from-intake`: Move a waiting file of the intake inbox onto a live case as a document of `purpose`, as an upload of the same file would be (the same checks apply: 409 `invalid_state` once a case is submitted, except `purpose: "other"`). The text of a forwarded email (`text/plain`) moves only as `correspondence`; any other purpose answers 415 `unsupported_media_type`. The file leaves the inbox. Moving a file already moved onto this case answers 200 with the same document; a test case answers 422 `live_only`
- `GET /v1/cases/{id}/documents/{docId}`: Download a document of the case
- `POST /v1/cases/{id}/validate`: Run the submission gates on a draft
- `POST /v1/cases/{id}/submit`: Submit a draft that passes every gate. A live case moves to `submitted`, its platform fee becomes a due charge, and we engage a licensed attorney in the property's state; nothing is served or filed until that attorney has reviewed it. A test case enters review by the built-in test reviewer
- `GET /v1/cases/{id}/authorization`: The authorization text of an agent-filed case, and whether it is signed and still current. 409 `not_agent_filed` for any other case
- `POST /v1/cases/{id}/authorization`: Sign the authorization of an agent-filed case, for the facts as they are now. Only while the case is a draft (409 `invalid_state`); 409 `not_agent_filed` for any other case. Signing again replaces the earlier signature
- `GET /v1/cases/{id}/signatures/complaint`: The complaint documents the owner must sign on an agent-filed case, and whether they are signed. 409 `not_required` when the county has us sign the complaint, or there is none yet; 409 `not_agent_filed` for any other case
- `POST /v1/cases/{id}/signatures/complaint`: Sign the complaint of an agent-filed case, where the county requires the owner to. Only while the case waits for the signature in `filing_approved`; 409 `not_required` otherwise, and 409 `not_agent_filed` for any other case. The filing is queued once. Where the filing is an affidavit sworn by electronic verification (`sworn` on the GET), only the named affiant signs, `accept: true` is the oath, `signerName` must be the affiant's name as printed (case and spacing aside; 422 `affiant_name_mismatch`), and `documentHashes` must be the `sworn.documents` hashes read (409 `documents_changed` when the affidavit changed since, for example on a new day): those documents become the filing
- `GET /v1/cases/{id}/signatures/complaint/documents/{specId}`: Download a sworn affidavit by its spec id. Before the oath (while the case waits for it in `filing_approved`): exactly as it will be filed if your named person swears it now, signed "/s/" with the affiant's name and dated today (the court's day). Once sworn: the affidavit that was sworn, as stored for the filing. 404 for any other document or case; 410 `erased` for an erased case
- `GET /v1/cases/{id}/tasks`: The outward steps of a full-service case, oldest first, each with its status and dates. 409 `not_agent_filed` for any other case
- `GET /v1/cases/{id}/events`: Customer-visible events of the case, oldest first
- `POST /v1/cases/{id}/report-cure`: Report that the tenant has paid or cured. For a live case the report is recorded and passed on to the attorney; the case is not closed automatically
- `POST /v1/cases/{id}/withdraw`: Withdraw the case. A live case with an attorney already engaged goes `on_hold` with a `withdrawal_requested` event instead, and is withdrawn once the attorney has confirmed that all work has stopped
- `GET /v1/cases/{id}/charges`: The charges on the case, oldest first, and whether online payment is available
- `POST /v1/cases/{id}/charges/{chargeId}/checkout`: Open a hosted checkout for one due charge. 409 `payments_unavailable` when online payment is off; 409 `charge_not_due` when the charge is paid, void or refunded; 409 `automatic_charge` for a charge taken from the saved card
- `POST /v1/cases/{id}/charges/{chargeId}/confirm`: Check with the payment provider whether the charge's checkout was paid, and record it if so. Safe to repeat. 409 `payments_unavailable` when online payment is off; 409 `automatic_charge` for a charge taken from the saved card
- `GET /v1/payment-methods`: The organization's saved cards for the automatic charges of full-service cases, in the key's mode: the default first, then newest first. Anyone in the organization can read them
- `POST /v1/payment-methods/setup`: Open the payment provider's page for adding a card. The card number is typed there and never reaches the API. The customer comes back to `returnPath` on the website with `setup_session` in the query (and `default=true` when `default` was asked for); send those to `confirm`. With a test key there is no card page and no real card: the returned URL only carries the setup's id in its `session` parameter; finish the setup with `POST /v1/payment-method/test-setup`, then `confirm` it. Needs the permission to manage payment methods. In live mode, 409 `live_unavailable` until live full-service cases are switched on; 503 `payments_unavailable` when online payment is off
- `POST /v1/payment-methods/confirm`: Add the card of a finished setup to the organization's cards in this mode. It becomes the default with `default: true`, or when the organization has no usable default. Every automatic charge still due is then retried shortly after, in the background. Safe to repeat: a card already added is answered as it is. 409 `setup_incomplete` while the card is not saved yet; 404 for a setup that is unknown, another organization's, or of a card removed since
- `PATCH /v1/payment-methods/{id}`: Name a card (`label`, null to clear it) or make it the default (`default: true`). 422 `payment_method_unusable` for a declined card made the default, and then nothing is changed. 404 for a card that is not one of the organization's in this mode. Each change is in the organization's activity log
- `DELETE /v1/payment-methods/{id}`: Remove a card: it is detached at the payment provider and never charged again. When it was the default, the newest card left that works becomes the default; with none left, a full-service case with a cost due waits until a card is added. 404 for a card that is not one of the organization's in this mode
- `GET /v1/charges`: Every charge of the organization's cases in this mode, newest first, optionally only one status, the charges created between two days (UTC), or the charges of one landlord's cases (`landlordId`, or `none` for the cases with no landlord), with the billing page's totals. Anyone in the organization can read them
- `GET /v1/charges.csv`: The same charges as a CSV file (at most 10,000 rows, newest first), named `charges-YYYY-MM-DD.csv` after the day of the request. Columns: created_at, charge_id, case_id, state, landlord, kind, description, amount_usd, status, paid_at, receipt_url. `landlord` is the display name of the case's landlord, empty when it has none. No tenant names, addresses or payment-provider ids
- `GET /v1/charges/by-landlord`: The organization's charges in this mode totalled per landlord of their cases (what a manager bills back to each owner): due, paid this calendar year (UTC), and how many charges. Only landlords with charges, archived ones included, by name; the cases with no landlord last, as "No landlord". The lines add up to the totals of `GET /v1/charges`. Anyone in the organization can read them
- `GET /v1/landlords`: The owners the organization acts for, by name, each with how many of its cases in this mode name them. Archived ones only with `includeArchived=true`. Anyone in the organization can read them with a live key or a signed-in session; a test key always gets an empty list, since landlords are real owners shared with the organization's live cases
- `POST /v1/landlords`: Save an owner the organization acts for. `ownerOfRecord` defaults to the name. Needs the permission to manage landlords. In the organization's activity log by its id only. 409 `landlord_limit` once the organization keeps 1,000 saved landlords, archived ones included. A test key gets 403 `forbidden`: landlords are real owners shared with the organization's live cases, so a test key neither sees nor changes them
- `GET /v1/landlords/{id}`: One landlord, archived or not, with its current management agreement. 404 `landlord_not_found` for one that is not the organization's, and for any landlord asked for with a test key
- `PATCH /v1/landlords/{id}`: Change a landlord, archived or not: only the fields given change; `contact` is replaced whole; null clears `plaintiffType` and `notes`. Cases already opened keep what they were given; a change that leaves it as it was is not logged. 404 `landlord_not_found` for one that is not the organization's. A test key gets 403 `forbidden`: landlords are real owners shared with the organization's live cases, so a test key neither sees nor changes them
- `POST /v1/landlords/{id}/archive`: Archive a landlord: its cases and charges stay, and it can no longer be chosen for a new case until it is unarchived. Never deleted. Archiving again changes nothing. A test key gets 403 `forbidden`: landlords are real owners shared with the organization's live cases, so a test key neither sees nor changes them
- `POST /v1/landlords/{id}/unarchive`: Unarchive a landlord, so it can be chosen for a new case again. A test key gets 403 `forbidden`: landlords are real owners shared with the organization's live cases, so a test key neither sees nor changes them
- `POST /v1/landlords/{id}/documents`: Upload the landlord's management agreement: a PDF, JPEG or PNG of at most 10 MB, decided by the file's contents (415 `unsupported_media_type` otherwise; 413 when larger). It replaces the current agreement; the earlier one is kept but no longer listed. 409 `landlord_file_limit` once the landlord has 20 files, replaced ones included. A test key gets 403 `forbidden`: landlords are real owners shared with the organization's live cases, so a test key neither sees nor changes them
- `GET /v1/landlords/{id}/documents/{docId}`: Download one of the landlord's files. 404 `landlord_not_found` for a landlord that is not the organization's, and for any landlord with a test key; 404 `not_found` for a file that is not the landlord's
- `GET /v1/payment-method`: The default card for the automatic charges of full-service cases, in the key's mode, or null when there is none. Kept for older clients: `GET /v1/payment-methods` lists every card
- `POST /v1/payment-method/setup`: Open the payment provider's page for saving a card. The card number is typed there and never reaches the API. The customer comes back to `returnPath` on the website with `setup_session` in the query; send that to `confirm`. With a test key there is no card page and no real card: the returned URL only carries the setup's id in its `session` parameter; finish the setup with `POST /v1/payment-method/test-setup`, then `confirm` it. In live mode, 409 `live_unavailable` until live full-service cases are switched on (the full service is offered only in test mode for now). 503 `payments_unavailable` when online payment is off
- `POST /v1/payment-method/confirm`: Add the card of a finished setup to the organization's cards in this mode, as the default card (kept for older clients: `POST /v1/payment-methods/confirm` chooses). Every automatic charge still due is then retried on it shortly after, in the background; a case on hold for a declined card continues once its costs are paid. Safe to repeat. 409 `setup_incomplete` while the card is not saved yet; in live mode, 409 `live_unavailable` until live full-service cases are switched on
- `POST /v1/payment-method/test-setup`: Test mode only (409 `sandbox_only` otherwise): finish a test setup with one of the two test cards, `success` (Visa ending 4242) or `decline` (ending 0002, which declines every charge). `sessionId` is the `session` parameter of the URL that `POST /v1/payment-method/setup` returned for the test key
- `GET /v1/cases/{id}/notes`: The messages on the case: the customer's own and ours, oldest first
- `POST /v1/cases/{id}/notes`: Send us a message about a live case. 409 `attorney_direct_only` for a test case. At most 20 every 10 minutes per organization (429 `rate_limited`)
- `POST /v1/cases/{id}/advance`: Test keys only: move a test case on by one step. Runs the step the case is waiting for now (the test reviewer's review, a service or filing update, or the end of the notice period on the case's own test clock), or records the next court outcome as the test reviewer. A live case (nothing about a real case is ever advanced automatically), or a case outside the fictional test state, gets 409 `sandbox_only`; a draft, a case on hold or a finished case gets 409 `nothing_to_advance`, as does a step whose last try failed, until its automatic retry (`error.details.retryAt`). While the step is already being taken, the case is returned as it is. At most 60 a minute per organization (429 `rate_limited`)
- `POST /v1/webhook-endpoints`: Register a webhook endpoint
- `GET /v1/webhook-endpoints`: List the organization's webhook endpoints in the credential's mode. The signing secret is never shown again
- `GET /v1/api-keys`: List the organization's API keys in the credential's mode, newest first, revoked ones included. The secret is never shown again
- `POST /v1/api-keys`: Create an API key in the credential's mode, optionally with a name. With `replaceSameName`, the active keys of the mode with that exact name are revoked first, so an app that connects again keeps one key. A live key can be created only by an owner of the organization, signed in with a session and `X-Evictions-Mode: live` (403 `forbidden` otherwise, also for a live API key); at most 10 active keys per mode, counted after any replaced keys (409 `key_limit`, and then nothing is revoked)
- `DELETE /v1/api-keys/{id}`: Revoke an API key of the organization and the credential's mode. A key cannot revoke itself (409 `cannot_revoke_current_key`)
- `DELETE /v1/webhook-endpoints/{id}`: Delete a webhook endpoint. Deliveries still waiting to be retried for it are dropped
- `GET /v1/organization`: The caller's organization and its members
- `PATCH /v1/organization`: Change the organization's profile: its name, kind and contact details (`organization.manage`, an owner by default) and the billing contact (`billing.manage`); a request with both needs both. Only the fields given change; null clears one. An API key gets 403
- `PATCH /v1/organization/members/{userId}`: Change a member's role (`owner`, `member`, `viewer`), or give them a `custom` list of permissions. Needs `people.manage`; only an owner makes, changes or removes an owner, and nobody gives a permission they do not hold. The last owner cannot be demoted. The person is emailed unless they changed their own role
- `DELETE /v1/organization/members/{userId}`: Remove a person from the organization. Needs `people.manage`; an owner is removed by an owner only, and never the last one. Their sessions acting for the organization are signed out, their other organizations stay, and they are emailed. Their cases, messages and signatures stay with the organization
- `GET /v1/organization/activity`: The organization's activity log: invitations, role changes, removals and profile changes, newest first. A signed-in person who manages people; an API key or anyone else gets 403
- `GET /v1/organization/invitations`: The organization's invitations. A signed-in person who manages people; an API key or anyone else gets 403
- `POST /v1/organization/invitations`: Invite a person by email, as an owner (owners only), member or viewer. A pending invitation to the same address is replaced. The link lasts seven days
- `POST /v1/organization/invitations/{id}/resend`: Email a pending invitation again with a new link. The old link stops working
- `DELETE /v1/organization/invitations/{id}`: Revoke a pending invitation. Its link stops working
- `POST /v1/auth/google`: Sign in with Google: the website trades the code Google gave it. Signs in to the account linked to that Google account, else links the account with the same email when it has no password (one with a password answers 409), else creates one (201) with no password. With `invitationToken` it also accepts that invitation: the Google account's email must be the invited address, a new account gets no organization of its own, and the session acts for the inviting organization. No credential; the website calls this from its servers
- `GET /v1/invitations/{token}`: An invitation, from the link in its email: the organization, the role and the invited address, masked. No credential; the token is the credential
- `POST /v1/invitations/{token}/accept`: Accept an invitation. Signed out, with a name and a password: makes the account for the invited address, confirmed, and answers 201 with a session for the organization. Signed in as the invited address, with no body: joins the organization and moves the session there (200). An address that already has an account must sign in to accept; its password is never set here
- `POST /v1/auth/logout`: Revoke the session in use
- `POST /v1/auth/password-reset/request`: Ask for a password reset link by email. No credential. The answer is the same whether or not the address has an account
- `POST /v1/auth/password-reset/confirm`: Set a new password with the token from a reset link. No credential. The token works once; every session of the user is revoked
- `POST /v1/auth/email-confirm/request`: Send a new "Confirm your email" link to the signed-in user's own address. The answer is always 202
- `POST /v1/auth/email-confirm/confirm`: Confirm the address with the token from the emailed link. No credential. The token works once. Confirming is never required for anything
- `GET /v1/me`: The signed-in person, the organization this session acts for, their role and permissions in it, and every organization they belong to
- `PATCH /v1/me`: Change the signed-in person's name
- `POST /v1/me/organization`: Act for another of the person's organizations. The session moves there, and the next sign-in starts there
- `POST /v1/me/organizations`: Start another organization, unverified, with the person as its owner; the session moves to it. Customer accounts only
- `POST /v1/me/organizations/{id}/leave`: Leave an organization. The person's other sessions acting for it are signed out; this one moves to their oldest remaining organization, or to none
- `PATCH /v1/me/organizations/{id}`: Choose which of the organization's case emails the person gets: `all`, `mine` (cases they opened) or `none`
- `POST /v1/me/password`: Change the person's password, with the current one. Every other session of the person is signed out (this one stays), and their pending email change and password reset links stop working
- `POST /v1/me/email`: Ask to change the person's email, with the current password. A link goes to the new address and the address changes only when it is opened; the old address is told
- `POST /v1/me/email/confirm`: Change the email with the token from the link sent to the new address. No credential. The token works once
- `POST /v1/me/sessions/revoke-others`: Sign out every other session of the person, keeping this one

### Public endpoints

No credential. Health, the OpenAPI document, the endpoint behind the website’s forms, and account sign-up and sign-in. Full reference: https://evictionsapi.com/docs/api/public.md

- `GET /v1/health`: Liveness check
- `GET /v1/openapi.json`: This OpenAPI document
- `POST /v1/auth/signup`: Create an account: an unverified customer organization, its owner and a session. No credential; the website calls this from its servers
- `POST /v1/auth/login`: Sign in with email and password. A wrong email and a wrong password get the identical `invalid_credentials` answer
- `GET /v1/public/pricing`: The prices in force: the platform fee for each starting step and the flat attorney fees of the published states. No credential; cached for a minute
- `POST /v1/public/leads`: Send a website request: access, a partner-attorney application, or a message. No credential; browser calls are allowed from the website origin only

### Attorney endpoints

Called with an attorney token to review a case and record its steps. An API key cannot call them. Customers integrating the API do not need them. Full reference: https://evictionsapi.com/docs/api/attorney.md

- `GET /v1/attorney/queue`: Cases awaiting the attorney's review, and assigned cases that need attention
- `GET /v1/attorney/cases/{id}`: A case with its documents, evidence, gate results, filings, service attempts and full audit trail; the access is audited
- `GET /v1/attorney/cases/{id}/documents/{docId}`: Download a document; the access is audited
- `GET /v1/attorney/counsel-assignments`: The calling attorney's limited counsel engagements: the one act each covers, its terms and the documents it grants
- `GET /v1/attorney/counsel-assignments/{id}/documents/{docId}`: Download a document an accepted engagement grants; the access is audited
- `POST /v1/attorney/cases/{id}/reviews`: Approve, reject or request changes, naming the document hashes reviewed
- `POST /v1/attorney/cases/{id}/outcomes`: Record a court outcome

## Error codes

| Code | Status | When |
| --- | --- | --- |
| `invalid_request` | 400, 422 | The request body, query or Idempotency-Key is invalid (400; details.issues lists each problem), or a draft change is not allowed, such as changing ground or entryPoint (422; details.field names it). |
| `invalid_reset_token` | 400 | POST /v1/auth/password-reset/confirm only: the token is unknown, was already used, or expired (a reset link lasts one hour). Ask for a new link. |
| `invalid_confirmation_token` | 400 | POST /v1/auth/email-confirm/confirm only: the token is unknown, was already used, or expired (a confirmation link lasts three days). Ask for a new link with POST /v1/auth/email-confirm/request. |
| `unauthenticated` | 401 | The Authorization header is missing, malformed or names an unknown or revoked credential. |
| `forbidden` | 403 | The credential is valid but not allowed here, for example an attorney endpoint called with an API key. |
| `permission_denied` | 403 | Signed-in sessions only: the person’s role in the organization does not include the permission this needs, for example a viewer opening a case. An owner can change their role. Nobody grants what they do not hold: POST /v1/organization/invitations and /resend, and PATCH /v1/organization/members/{userId}, also answer it when someone who is not an owner invites or assigns a role with a permission they lack; only an owner makes, changes or removes an owner. An API key gets it from PATCH /v1/organization. |
| `account_kind` | 403 | POST /v1/me/organizations, POST /v1/invitations/{token}/accept, and POST /v1/auth/google with an invitationToken: an attorney’s or the operator’s account cannot start or join an organization. POST /v1/me/password and POST /v1/me/email: those accounts cannot change their password or email there. |
| `wrong_password` | 403 | POST /v1/me/password and POST /v1/me/email: the current password is not correct. Nothing was changed. Five wrong ones for an account in 15 minutes lock it for a while, sign-in included. |
| `no_organization` | 403 | Signed-in sessions only: the person belongs to no organization yet. Start one with POST /v1/me/organizations, or accept an invitation. |
| `live_unavailable` | 403, 409 | Live mode is switched off on this server, and the request asked for it: a signed-in session sent X-Evictions-Mode: live, or a live API key was used or requested (403). POST /v1/payment-method/setup and /confirm, and every change to /v1/payment-methods, in live mode while live full-service cases are not switched on (409): the full service works only with test keys for now. |
| `attorney_not_eligible` | 403 | Attorney endpoints only: the assigned attorney is not verified or not licensed in the property’s state. |
| `invitation_invalid` | 404 | GET /v1/invitations/{token}, POST /v1/invitations/{token}/accept, and POST /v1/auth/google with an invitationToken: the invitation is unknown, already accepted, revoked or expired (a link lasts seven days). Ask the person who invited you for a new one. |
| `email_change_invalid` | 404 | POST /v1/me/email/confirm only: the token is unknown, was already used, was replaced by a newer request, or expired (an email change link lasts 24 hours). Ask for a new one with POST /v1/me/email. |
| `not_found` | 404 | The case, document, API key, webhook endpoint or saved card does not exist for your organization and mode, or the route does not exist. |
| `invalid_state` | 409 | The case is no longer a draft, so its facts cannot change and evidence cannot be added. |
| `invalid_transition` | 409 | The action is not allowed in the case’s current state, for example submitting a case that is not a draft. |
| `erased` | 409, 410 | The details of the case were erased, which we do case by case on request. Any request that would change the case answers 409; downloading one of its documents answers 410. The case can still be read. |
| `guard_failed` | 409 | A step needs something that is missing, such as a current attorney approval of the exact documents. |
| `stale_approval` | 409 | Attorney endpoints only: the approved document hashes do not match the current documents. |
| `not_agent_filed` | 409 | The authorization and complaint-signature endpoints, and PATCH /v1/cases/{id} with paymentMethodId: the case is not a full-service case, where we act as your authorized agent, so it has nothing to sign and is not charged to a saved card. |
| `agent_filing_refused` | 409 | POST /v1/cases/{id}/submit on a full-service case: we cannot act on the case yet. details.reason says why: for example authorization_missing or authorization_stale (sign the authorization again), card_missing (save a card), complaint_signer_not_permitted (where the client signs the court filing, name for sign_complaint a member the court allows: the plaintiff in person or its own officer or full-time employee, never our staff), disabled, state_closed, state_paused or pack_missing (full service is not open for that state, and nothing was done). |
| `jurisdiction_unavailable` | 409 | GET and POST /v1/cases/{id}/authorization, and submit: no rules cover the case, so there is no authorization text and nothing to submit. |
| `not_required` | 409 | The complaint-signature endpoints only: there is nothing for you to sign now. The county has us sign the complaint, or the complaint is not ready yet (the case is not in filing_approved), or it has no documents. |
| `documents_changed` | 409 | POST /v1/cases/{id}/signatures/complaint where the filing is an affidavit sworn by electronic verification: documentHashes are not the hashes of the affidavit as it would be filed now (it changed since it was read, for example on a new day, or none were sent). Nothing was signed. Read it again (GET the signature, then its sworn.documents), then swear to it. |
| `affiant_name_mismatch` | 422 | POST /v1/cases/{id}/signatures/complaint where the filing is an affidavit sworn by electronic verification: the typed signerName is not the affiant's name as the affidavit prints it (sworn.affiantName; case and spacing aside). details.field is signerName. Nothing was signed. |
| `already_member` | 409 | POST /v1/organization/invitations and /resend, and accepting an invitation: the address belongs to a person who is already a member of the organization. |
| `member_limit` | 409 | POST /v1/organization/invitations and /resend, and accepting an invitation: the organization already has 50 people. Write to hello@evictionsapi.com if you need more. |
| `invitation_email_mismatch` | 409 | POST /v1/invitations/{token}/accept and POST /v1/auth/google with an invitationToken: the invitation was sent to another address than the signed-in account’s or the Google account’s. Nothing was changed. |
| `email_taken` | 409 | POST /v1/auth/signup, POST /v1/me/email and POST /v1/me/email/confirm: an account with that email address already exists. Nothing was changed. |
| `organization_changed` | 409 | Signed-in sessions only, on a change sent with X-Evictions-Organization (our website sends it): the session now acts for another organization, switched in another tab. Nothing was changed. Reload the page and try again. A request without the header, and every API key, is not checked. |
| `no_password` | 409 | POST /v1/me/password and POST /v1/me/email: the account signs in with Google and has no password, so neither can be changed there. |
| `last_owner` | 409 | POST /v1/me/organizations/{id}/leave, PATCH /v1/organization/members/{userId} (a demotion) and DELETE /v1/organization/members/{userId}: the person is the organization’s last owner. Make someone else an owner first. |
| `landlord_limit` | 409 | POST /v1/landlords only: the organization already keeps 1,000 saved landlords, archived ones included. Nothing was saved. Write to hello@evictionsapi.com if you need more. |
| `landlord_file_limit` | 409 | POST /v1/landlords/{id}/documents only: the landlord already has 20 files, replaced ones included. Nothing was saved. Write to hello@evictionsapi.com if you need more. |
| `key_limit` | 409 | POST /v1/api-keys only: the organization already has 10 active API keys in this mode. Revoke one first. |
| `cannot_revoke_current_key` | 409 | DELETE /v1/api-keys/{id} only: an API key tried to revoke itself. Use another key, or sign in. |
| `sandbox_only` | 409 | POST /v1/cases/{id}/advance: the case is not a test case in the fictional test state. Live cases are never advanced. POST /v1/payment-method/test-setup: the request is in live mode, where there are no test cards. |
| `nothing_to_advance` | 409 | POST /v1/cases/{id}/advance only: the test case is a draft, on hold or finished, or is not waiting on anything a test step can move on. |
| `payments_unavailable` | 409, 503 | POST /v1/cases/{id}/charges/{chargeId}/checkout and /confirm (409): online payment is switched off on this server, and the charge stays due. POST /v1/payment-method/setup and /confirm, and POST /v1/payment-methods/setup and /confirm, in live mode (503): a card cannot be saved while online payment is off. |
| `payments_error` | 502 | The checkout and payment-method endpoints (/v1/payment-method and /v1/payment-methods): the payment service did not answer or refused the request. Nothing was charged and any charge stays due. Try again. |
| `automatic_charge` | 409 | POST /v1/cases/{id}/charges/{chargeId}/checkout and /confirm: the charge is a cost of a full-service case. It is taken from the saved card, so it is not paid by checkout. |
| `setup_incomplete` | 409 | POST /v1/payment-method/confirm and POST /v1/payment-methods/confirm only: the card has not been saved on the payment provider's page yet, or that page expired. details.status says which. |
| `idempotency_conflict` | 409 | The Idempotency-Key was already used with a different request or credential. |
| `payload_too_large` | 413 | A document is over 10 MB after decoding, or the request body is too large. |
| `unsupported_media_type` | 415 | The request body is sent with a content type the API does not accept, or an uploaded document is not a PDF, a JPEG or a PNG. |
| `payment_method_unusable` | 422 | PATCH /v1/payment-methods/{id} with default: true: the card was declined, so it cannot be the default. Nothing was changed. Add another card or choose one that works. |
| `landlord_not_found` | 404, 422 | GET, PATCH /v1/landlords/{id}, its /archive, /unarchive and /documents routes (404): the landlord does not exist for your organization. POST /v1/cases and PATCH /v1/cases/{id} with landlordId (422): the landlord is not one of your organization’s active landlords (an archived one cannot be chosen for a case), and nothing was changed. The same answer for one that never existed and for another organization’s. Landlords are real owners, so with a test key every landlord read answers 404 (and GET /v1/landlords an empty list), and a test case cannot take one (422). |
| `payment_method_not_found` | 422 | PATCH /v1/cases/{id} with paymentMethodId: the card is not one of your organization’s saved cards in this mode that can be charged (it may have been removed or declined). Nothing was changed. Choose a card from GET /v1/payment-methods, or null for the default card. |
| `gates_failed` | 422 | Submit only: the case did not pass the submission checks. details holds the check results. |
| `signer_unnamed` | 422 | POST /v1/cases and PATCH /v1/cases/{id} with declared facts: the member named to sign the court filing for you has no full name on their account (an email address or a name without a letter is not one). The court filing prints the signer's name, so the person sets their full name on their Account page first. details.field names the actor. Nothing was stored. |
| `signer_not_member` | 422 | POST /v1/cases and PATCH /v1/cases/{id} with declared facts: the person named to sign the court filing for you (an actor with sign_complaint whose relationship is not platform_employee) is not a member of your organization. Name them by their user id from GET /v1/organization; a typed name is not accepted for this role. details.field names the actor. Nothing was stored. |
| `prior_notice_missing` | 422 | A prior-notice document was uploaded before the case recorded priorNotice. |
| `documents_unavailable` | 422 | Submit only: the jurisdiction has no documents for this ground and entry point. |
| `invitation_rate_limited` | 429 | POST /v1/organization/invitations and /resend: the organization sent 20 invitations in the last 24 hours, or the person sent 20 across all their organizations, resent and replaced ones included. Nothing was sent. Please try again later. |
| `rate_limited` | 429 | Too many requests: more than 5 to POST /v1/public/leads from one address in 10 minutes, more than 10 sign-ups and sign-ins from one address in 10 minutes, a sign-in to an account after 5 wrong passwords for it in 15 minutes (whatever the password), more than 10 password reset requests from one address in 10 minutes, a password or email change after 5 wrong current passwords for the account in 15 minutes, more than 10 email change requests and email change links opened, counted together, from one address in 10 minutes, more than 3 email change links sent for one account in an hour, more than 5 organizations started by one person in 24 hours (POST /v1/me/organizations, the one made at sign-up included), more than 60 test-case advances per organization in a minute, or more than 20 messages on cases per organization in 10 minutes. Retry-After gives the seconds to wait. |
| `busy` | 503 | POST /v1/auth/signup, POST /v1/auth/login, POST /v1/auth/password-reset/confirm, POST /v1/me/password and POST /v1/me/email only: too many password checks are waiting. Nothing was created or changed. Retry-After: 5 gives the seconds to wait. |
| `internal` | 500 | An unexpected error. The response carries no detail. |

## Rate limits

| Endpoints | Limit |
| --- | --- |
| POST /v1/public/leads | 5 requests per address every 10 minutes |
| POST /v1/auth/signup, POST /v1/auth/login and POST /v1/auth/google | 10 requests per address every 10 minutes, counted together |
| POST /v1/auth/login, POST /v1/me/password and POST /v1/me/email, per account | 5 wrong passwords per account every 15 minutes, counting sign-ins and the current password given to change the password or email. Past that, signing in to the account, or changing its password or email, answers 429 whatever the password, until the oldest failure is 15 minutes old. A successful sign-in, or a right current password, clears the count |
| POST /v1/auth/password-reset/request and POST /v1/auth/password-reset/confirm | 10 requests per address every 10 minutes, counted together and apart from sign-up and sign-in |
| POST /v1/auth/password-reset/request, per account | 3 reset emails per account every hour. A request past that is still answered 202, not 429, and sends no email |
| POST /v1/auth/email-confirm/request and POST /v1/auth/email-confirm/confirm | 10 requests per address every 10 minutes, counted together and apart from sign-up, sign-in and password reset |
| POST /v1/auth/email-confirm/request, per account | 3 confirmation emails per account every hour, the one sent at sign-up included. A request past that is still answered 202, not 429, and sends no email |
| POST /v1/me/email and POST /v1/me/email/confirm | 10 requests per address every 10 minutes, counted together and apart from the sign-in, password reset and confirmation limits |
| POST /v1/me/email, per account | 3 email change links per account every hour. A request past that answers 429 and sends nothing |
| POST /v1/organization/invitations and POST /v1/organization/invitations/{id}/resend, per organization | 20 invitations per organization in any 24 hours, resent and replaced ones included. A request past that answers 429 invitation_rate_limited and sends nothing |
| POST /v1/organization/invitations and POST /v1/organization/invitations/{id}/resend, per person | 20 invitations per person in any 24 hours, counted across all their organizations. A request past that answers 429 invitation_rate_limited and sends nothing |
| POST /v1/me/organizations, per person | 5 new organizations per person in any 24 hours, the one made at sign-up included. A request past that answers 429 and creates nothing |
| POST /v1/cases/{id}/advance | 60 requests per organization every minute |
| POST /v1/cases/{id}/notes | 20 requests per organization every 10 minutes |
| POST /v1/geo/county | 60 requests per organization every hour |
| POST /v1/intake/files | 60 requests per organization every hour |
| POST /v1/intake/email, the email Worker's delivery route, per organization | 60 accepted messages per organization every hour. The route is internal: our email Worker calls it for mail sent to an intake address, not with an API key; it exists only when email intake is set up, and it is not in the API document. A message past the limit is answered 200, not 429, and nothing is stored or emailed |
| POST /v1/integrations/{system}/connect and POST /v1/integrations/{system}/leases | 60 requests per organization every hour, counted together |
| POST /v1/integrations/{system}/pull | 10 requests per organization every hour |
| POST /v1/extract | 20 requests per organization every hour (one lease, one ledger PDF, or one set of up to 10 messages; a refused request counts too) |

Over a limit the answer is 429 `rate_limited` with a `Retry-After` header in seconds. Three limits answer without a 429: past their limits, password-reset and confirmation emails are simply not sent (the request is still answered 202, so the answer never shows whether an address has an account), and the email Worker's deliveries are answered 200 with nothing stored. No other endpoint is rate limited today.
