# Lawline API Documentation `https://api.dev.lawline.se` Lawline is a legal AI agent for Swedish and EU law. Lawline's agent researches primary legislative sources and answers with verifiable citations. The Lawline API exposes Lawline's legal agent over an HTTP API ([`/v1`](/v1.md)) and an MCP server ([`/mcp`](/mcp.md)). An embedded widget ([`/iframe`](/iframe.md)) is coming soon. Access is managed by the [Lawline Platform](https://platform.dev.lawline.se). See the [connection and billing guide](/getting-started.md). ## Endpoints | Surface | Path | Use-case | | --- | --- | --- | | HTTP API | [`/v1`](/v1.md#quickstart) | Call the legal agent directly. Its chat endpoint is OpenAI-compatible, so an OpenAI SDK works unchanged. | | MCP server | [`/mcp`](/mcp.md#quickstart) | Give an existing agent Lawline as a legal tool. | | Embedded widget | [`/iframe`](/iframe.md) | Coming soon: Lawline directly inside a web page. | The [OpenAPI 3.1 document](https://api.dev.lawline.se/v1/openapi.json) describes every `/v1` operation, and [`/v1/health`](https://api.dev.lawline.se/v1/health) answers while the API is up. Neither needs an API key. Every page of these docs is also plain markdown for coding agents and other tools: start at [`/llms.txt`](/llms.txt). Changes and deprecations are in the [changelog](/changelog.md). The [status page](/status.md) shows whether the API answers right now, and lists past incidents. ## Authentication Use an API key for `/v1` or `/mcp`. Create and manage keys in the [Lawline Platform](https://platform.dev.lawline.se). Send `Authorization: Bearer llk_...` and select the required scope. For MCP, OAuth is also available when enabled in the environment. OAuth grants `mcp` access only. See the [connection and billing guide](/getting-started.md). A key is rejected with `403` if out of scope. Revoked and expired API-keys return `401`. An expired key carries `code` `expired_api_key` and `details.expired_at`, so it is not mistaken for a wrong key. A key can be issued without an expiry. ## Legal Sources The legal agent searches the following sources. All are always active. | Key | Coverage | | --- | --- | | `sfs` | Swedish statutes | | `proposition` | Preparatory works, including provision commentary. | | `case` | Swedish cases and precedent (e.g. NJA). | | `eu` | EU regulations, directives, and decisions (e.g. GDPR). | | `boverket` | Building regulations (BBR, EKS, OVK). | | `authority` | Agency regulations not in SFS (SKVFS, AFS, SOSFS, TSFS, SKOLFS). | | `skatteverket` | Tax authority guidance. | | `qa` | Lawline's Q&A database. | A citation names the corpus it came from in `type`. The value matches the key above except for statutes, which carry `sfs2`, and tax guidance, which carries `authority`. ## Idempotency `POST /v1/chat/completions` and `POST /v1/review` accept an `Idempotency-Key` header: 1 to 255 printable ASCII characters, chosen by you. On `/v1/chat/completions` the first request with a key runs and its answer is kept in the API database for 24 hours, and is removed with your tenant; a repeat of the same request returns that answer with `Idempotent-Replayed: true` and is not run or billed again. Each replay, also on `/v1/review` and on background answers, carries `Idempotent-Replay: true` as well. It is a deprecated alias of the same header, so read `Idempotent-Replayed`. A repeat while the first is still running returns `409 idempotency_in_progress` with `Retry-After`; the same key over a different body returns `409 idempotency_key_reused`; a key whose answer was above the 256 KB kept for replay returns `409 idempotency_not_replayable`, and needs a new key. Each carries `details.original_request_id`. A key that a run already holds, from `/v1/review` or a background answer, returns `409 idempotency_key_reused` with `details.original_run_id` instead. A request that failed, or whose connection you dropped before the answer was whole, gives its key back, so a retry runs; what the first attempt ran is metered as a cancellation. A failure inside a streamed answer counts as a failure even though the status line already said 200. On `/v1/review` the key names the review's run, and a repeat of the same request reaches that run for as long as it is kept, seven days after it ends: it follows the run while it is going and hands back its result once it is done, with a freshly signed report link. Nothing is run or billed again. A repeat that reaches the run carries `Idempotent-Replayed: true`. A completed run keeps its key, so use a new key for a new review; the same key over a different body returns `409 idempotency_key_reused` with `details.original_run_id`. A run that failed or was cancelled gives its key back, so a retry runs. If the review cannot be handed on, the answer is `503` with the run in `details.run_id`; with a key, repeating the request hands it on, and the service also picks it up by itself within minutes. Without a key, repeating a request with the same body while its run is still queued or running reaches that run; once it has ended, the repeat starts and bills a new review. So send a key with any review you may retry. On `/v1/chat/completions` with `background: true`, where background answers are available, the key names the answer's run. The rules of `/v1/review` above apply. Where background answers are no longer available, a repeat with the same key and body still gets its run with `200`. A conflict over a key that a run holds carries `details.original_run_id`, and one over a key that an inline answer holds carries `details.original_request_id`. One key names one request: the same key over any other body returns `409 idempotency_key_reused`, also when only `background` differs. So a key used for an inline answer is refused for a background answer, and the reverse. A key that an inline answer received before background answers were available keeps that answer, and a repeat gets it back. Without a key, a repeat starts and bills a new answer. The one exception is an automatic retry from an OpenAI SDK, which reaches a run with the same body from the same API key. That run started at most 30 seconds earlier for each retry the SDK counts, and at most two minutes earlier. It is the run that the lost response described, unless you sent identical requests at the same time. So send a key with every background answer. ## Agent Memory Shape legal responses by modifying the agent's memory. On the **Memory** page of the Lawline Platform, the **Context** block can be set to inject policies, terminology, and rules up to 20,000 characters. This is what shapes the answers. **Files** uploaded on the same page are organisation storage. The agent does not read them, so they do not affect responses. Agent memory applies to all requests across all endpoints. ## Limits & Billing Usage is metered in **Lawline tokens**. **Usage** can be tracked and visualised in the Lawline Platform. All new organisations have the standard tier by default. Contact Lawline to upgrade. | Tier | Requests Per Minute | Lawline Tokens Per Month | Active Reviews | Active Answers | | --- | --- | --- | --- | --- | | Standard | 60 | **100M** | 3 | 20 | | Enterprise | 600 | **10B** | 20 | 60 | Exceeding the request rate returns `429` with a `retry_after` (seconds). Exceeding the monthly quota returns `402`. An organisation without active billing is rejected with `402`. A review on `POST /v1/review`, and a chat answer with `background: true`, run on the server as runs. Your tier allows the active reviews and active answers in the table, queued or running at once. Each kind counts only its own runs, so reviews and answers never block each other. One more returns `429 too_many_active_runs` with `Retry-After`: 30 seconds for a review and 5 for an answer. Reading or following a run is not checked against the monthly quota or billing, so a result you already paid for stays readable, but it counts against your requests per minute. A repeat that reaches its run, with the same key or as an OpenAI SDK retry, is a read in this sense. Cancelling a run is checked against none of the three, and its response carries the shared ceiling's `RateLimit-*` numbers. The MCP tool `review_document` also creates a server-owned run and counts toward the same limit of active reviews. `get_review` reads that run without new model work or a billing/quota check. It counts against your request rate. `cancel_review` stops the run without a billing, quota or tenant request-rate check. Where answers run on the server, the MCP tool `lawline-v2` also creates a server-owned run and counts toward the same limit of active answers. `get_answer` reads it like `get_review`, and `cancel_answer` stops it like `cancel_review`. Authentication, scope and the shared IP limit still apply. A second ceiling of 1000 requests per minute per source IP applies ahead of the tier limit, shared by everyone behind the same egress address. Each server instance counts this ceiling on its own. The API can run on several instances, so one source IP can get more than 1000 requests per minute through. Pace against your tier's limit, not against this ceiling. A response that reached your tier's limiter carries `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` and `RateLimit-Policy` for your per-minute window. A request rejected before it, which is any `401`, `402`, `403`, `413` or a `429` from the shared ceiling, carries that ceiling's numbers instead. The instance that answered counted these numbers, so `RateLimit-Remaining` from the ceiling can go up between two responses in the same window. `RateLimit-Limit` tells the two apart: your tier's limit is the one in the table above. ## Errors Errors return a JSON `error` object with `type`, `code`, `message`, `request_id`, and on some errors `param` and `details`. `param` names the request field that the error is about, as a dot path, for example `messages.1.role`. `type` is the value in the table of types below. `code` is a narrower machine string. It equals `type` when there is no narrower cause. The table of codes below lists every other `code` that the API sends. New codes can be added, so use `code` to tell cases apart, and handle a `code` that you do not know by its `type`. On `402 payment_required`, `code` is the billing status of the organisation (`pending`, `past_due`, `canceled`). `details` carries `retry_after` (seconds) on `rate_limited`, `used` and `limit` on `quota_exceeded`, and `max_bytes` on `payload_too_large`. A `429` also sets the standard `Retry-After` response header. On `too_many_active_runs` it also carries `limit`, the most runs of that kind your tier allows at once, and `kind`, the kind of run that reached it (`review` or `answer`). A background answer sent without a key that gets this error also has the header `x-should-retry: false`, so an OpenAI SDK does not retry it. So do `background_unavailable`, `idempotency_key_reused` and `idempotency_not_replayable`, which a retry meets again, and `request_timeout`, which a retry runs and bills again. On `409 conflict` it carries `original_request_id` when an inline request holds the `Idempotency-Key`, and `original_run_id` when a run holds it. On the `/v1/review` stream, the error that ends a review carries the run in `run_id`. On `POST /v1/chat/completions` and `POST /v1/review`, a top-level field with the value `null` counts as absent, so its default applies. A field that the endpoint does not take returns `400` with `code` `unsupported_parameter`. `details` names each such field, up to 20, and `param` names the first. If there are more than 20, `message` gives the number of the other fields. If the body has another error too, the answer is `422` `validation_failed`, and `details` names up to 20 fields that failed, the unsupported ones included. A request body has a size cap. The cap is 1 MB, and 15 MB on the endpoints that take a document, which are `/v1/review` and `/mcp`. A body above the cap gets `413`. On `/v1`, a body without `Content-Length` (chunked transfer-encoding, or an HTTP/2 client that omits the header) is accepted, and its bytes count against the cap as they arrive. On `/mcp`, a request body must declare `Content-Length`. A body without it gets `400` `missing_content_length`, and a malformed length gets `400` `invalid_content_length`. The document in a review has its own cap of 10 MB after base64 decoding, and a larger document also gets `413`. | Status | Type | Meaning | | --- | --- | --- | | 400 | `bad_request` | Malformed request, or a field that the endpoint does not take (`code` `unsupported_parameter`). | | 401 | `unauthorized` | Missing or invalid API key. `code` is `expired_api_key` when the key was valid and has expired, with `details.expired_at`. | | 402 | `payment_required` | No active billing. | | 402 | `quota_exceeded` | Monthly Lawline-token quota reached. | | 403 | `forbidden` | Key lacks the required scope. | | 404 | `not_found` | No such route, an unsupported method on a documented path, or a run that is not yours or does not exist. | | 409 | `conflict` | An `Idempotency-Key` that is still running, was used for a different request, or whose answer is gone. | | 413 | `payload_too_large` | Request body above the endpoint's cap, or a review document above 10 MB. `details.max_bytes` names the cap that applied. | | 422 | `validation_failed` | Request failed validation. `details` names each field that failed, up to 20, and `param` names the first. | | 429 | `rate_limited` | Too many requests. | | 503 | `upstream_unavailable` | The model provider or another service that the request needs is not available, or the answer did not complete. `code` names the cause. | | 500 | `internal` | Unexpected error. | A `500` or a `503` points at Lawline or its model provider, not at your request. [Status and incidents](/changelog.md#status) says how to tell an incident from a fault in your request, and what to send Lawline about one. One more `type`, `tool_failed`, has no status. It comes only in an MCP tool result, when the tool name or the arguments are not valid. Some codes arrive in a response that already started: in an `error` event of a stream, in the `error` of a run, or in an MCP tool result. These carry the same `type` and `code`, and the table gives the status of their `type`. | Code | Status | Meaning | | --- | --- | --- | | `invalid_content_type` | 400 | The body is not sent as JSON. Send `Content-Type: application/json`. | | `missing_content_length` | 400 | On `/mcp`, the body does not declare `Content-Length`. | | `invalid_content_length` | 400 | On `/mcp`, `Content-Length` is not a number of bytes. | | `invalid_idempotency_key` | 400 | `Idempotency-Key` is blank, or has a character that is not printable ASCII. | | `invalid_starting_after` | 400 | `starting_after` or `Last-Event-ID` is not a sequence number. | | `unsupported_parameter` | 400 | The body has a field or a value that the endpoint does not take. Remove it. | | `background_unavailable` | 400 | `background: true` where background answers are not available. Send the request without `background`. | | `background_stream_unavailable` | 400 | `background: true` with `stream: true`. Send only one of them. | | `invalid_document` | 400 | The document is not base64, not a PDF, encrypted, or has no text that can be read. A scan needs OCR first. Send an unencrypted PDF with a text layer. | | `document_too_long` | 400 | The PDF has more than 30 pages. Split it into smaller documents. | | `prompt_injection_blocked` | 400 | The safety check stopped text that tries to reveal or replace the agent's instructions. It reads the last user message, and on a review the `instructions` and the `document`. Change that text. | | `mcp_session_required` | 400 | On `/mcp`, a tool call came without the session from `initialize`. Initialize the session first. | | `mcp_request_id_used` | 400 | On `/mcp`, the JSON-RPC request id is already used or cancelled. Use a new id. | | `run_cancelled` | 400 | The run that the request waited for was cancelled. | | `missing_bearer_token` | 401 | The request has no `Authorization: Bearer` header. | | `invalid_api_key` | 401 | The API key does not exist or is revoked. | | `expired_api_key` | 401 | The API key expired at `details.expired_at`. Create a new key. | | `scope_required` | 403 | The API key does not have the scope for this surface, `v1` or `mcp`. | | `run_not_found` | 404 | Your organisation has no run with this id. | | `idempotency_in_progress` | 409 | A request with this key still runs. Send the request again after `Retry-After` seconds. | | `idempotency_key_reused` | 409 | The key belongs to a different request. Use a new key. | | `idempotency_not_replayable` | 409 | The answer to the request with this key was too large to keep. Use a new key. | | `too_many_active_runs` | 429 | Your organisation has the most active runs of this kind that its tier allows. Send the request again after `Retry-After` seconds. | | `run_attempts_exhausted` | 500 | The run failed in each attempt. Start a new run. | | `run_result_not_storable` | 500 | The run finished, but the service could not store its result. Start a new run. | | `request_timeout` | 503 | The time ran out. A non-streamed chat answer stops after 4 minutes, so stream it or use `background: true`. On `/v1/review`, the review continues: read its run. A background answer fails after 13 minutes. | | `empty_answer` | 503 | The model gave no text. Send the request again. | | `incomplete_answer` | 503 | The model stopped before the answer was complete. Without `stream`, no part of it is sent. With `stream`, discard the content chunks that came before the `error` event. Send the request again. | | `run_result_unavailable` | 503 | The review finished, but the report link could not be made. Read the run again. |