# Get started with the LexSelect API

Turn documents into structured text, tables and document sections. Use the JSON tree for layout-aware applications, word coordinates for highlighting, or text and paragraphs for your own search and extraction pipeline. Supported inputs: PDF, DOC, DOCX, ODT, RTF, EML and MSG. Text recognition covers more than 200 languages.

[API reference](/api/docs) · [Understand output](/api/understanding-output.html) · [Operations and limits](/api/operations.html) · [Webhooks](/api/webhooks.html) · [Versions and changelog](/api/versioning.html)

## Plan for these first

- **Retention.** API uploads are deleted 24 hours after upload or 1 hour after processing ends, whichever comes first. Save results in your own system as soon as processing completes.
- **Ownership.** Your account is the user account that creates your API keys. Documents, API keys and webhook endpoints belong to the account that created them, and membership in a shared Enterprise workspace doesn't give access to another member's. Run your integration under a dedicated account, and use separate accounts for development and production.
- **Webhooks plus a sweep.** Use [signed webhooks](/api/webhooks.html) for speed, and also run a [reconciliation sweep](/api/operations.html) every few minutes to catch missed events and canceled runs. Real events are delivered only to receivers on a hostname you verify once with a DNS TXT record, so schedule that DNS change early.
- **Versions.** Send `X-API-Version: 2026-11-05` on every request to keep that version's response shape; see [Versions and changelog](/api/versioning.html).
- **Email files.** An EML or MSG upload produces child documents for the message body and each attachment. Read results from the children; see [Understand output](/api/understanding-output.html).

## Base URL and authentication

The base URL is **https://api.lexselect.io/api**. Paths in these guides are relative to it.

Create an API key in [Studio → API keys](https://studio.lexselect.io/keys). A key expires on the date you choose, at most 365 days after creation.

Studio shows the secret once. Store it in your server's secret store and send it as `Authorization: Bearer lxs_...`. The API doesn't accept cross-origin browser requests, so call it from your server, and keep the key out of browsers, URLs, logs and source repositories.

To rotate a key, generate a new secret in Studio and deploy it. The previous secret keeps working for 24 hours, or until the key expires if that's sooner. Revoking a key disables it and its previous secret; your documents stay in place.

## First document: upload, wait, read

The current API version is `2026-11-05`. These commands use `curl` and `jq`.

```sh
set -eo pipefail
export LEXSELECT_API_URL=https://api.lexselect.io/api
export LEXSELECT_API_KEY=lxs_replace_with_your_secret
export UPLOAD_OPERATION_ID="$(uuidgen)"

DOCUMENT_ID="$(curl --fail-with-body --silent --show-error \
  -H "Authorization: Bearer $LEXSELECT_API_KEY" \
  -H 'X-API-Version: 2026-11-05' \
  -H "Idempotency-Key: $UPLOAD_OPERATION_ID" \
  -F "name=upload-$UPLOAD_OPERATION_ID.pdf" \
  -F 'strip_file_name=false' \
  -F 'file=@./example.pdf' \
  "$LEXSELECT_API_URL/documents/upload" | jq -r .id)"
echo "$DOCUMENT_ID"
```

The fields are `name` and `file` (required), and `parent_id`, `strip_file_name`, `content_type` and `size` (optional); send them all before the `file` part. With the file's exact byte count in `size`, the file is stored while it is being received, so large uploads finish sooner. Without `strip_file_name=false`, the stored name is generated.

The response is the created document (abridged). `parent_id` is the folder it went into, your Default Project when you don't send one:

```json
{
  "id": "6650c0ffee0000000000a001",
  "type": "file",
  "name": "upload-7b07818a-72ad-4b48-9cba-e09a9fb3d581.pdf",
  "parent_id": "6650c0ffee0000000000a003"
}
```

Each upload is one operation with its own `Idempotency-Key` and unique `name`. If the request times out or returns `500`, `503` or `504`, the document may exist: look it up by that name before you retry, and retry with the same key and name if it isn't there. After any other error, upload again as a new operation with a new key. [Retry safely](/api/operations.html) gives the exact steps.

Check the document's processing status:

```sh
curl --fail-with-body --silent --show-error \
  -H "Authorization: Bearer $LEXSELECT_API_KEY" \
  -H 'X-API-Version: 2026-11-05' \
  "$LEXSELECT_API_URL/documents/$DOCUMENT_ID/processing/latest"
```

| `status`      | Meaning                                                                                         |
| ------------- | ----------------------------------------------------------------------------------------------- |
| `in_progress` | Queued or processing.                                                                           |
| `completed`   | Results are ready.                                                                              |
| `failed`      | Processing failed; `error_code` says why (see [Operations and limits](/api/operations.html)).   |
| `unknown`     | The run was canceled and won't continue. Upload again, as a new operation, if you need results. |

After an upload, this endpoint can return `404` with `code: PROCESSING_NOT_FOUND` while processing is being created. Retry with backoff until your application's deadline. If it persists, contact [support@lexselect.io](mailto:support@lexselect.io) with the document ID and `X-Request-Id` before uploading another copy.

Poll with a growing delay: start at about two seconds, increase to about 15 seconds, add jitter, and give up at a deadline that fits your application. A `429` with a `code` means a configured limit stopped the request, so don't retry it automatically. A `429` without a `code` is a rate or concurrency limit: wait for `Retry-After` when it's present, otherwise back off and lower your concurrency. For more than a handful of documents, use [signed webhooks](/api/webhooks.html) instead of polling.

When `status` is `completed`, fetch the results:

```sh
curl --fail-with-body --silent --show-error \
  -H "Authorization: Bearer $LEXSELECT_API_KEY" \
  -H 'X-API-Version: 2026-11-05' \
  "$LEXSELECT_API_URL/documents/$DOCUMENT_ID/parse?include=text,paragraphs"
```

Store the results right away; the document is deleted on the retention schedule above. Reading results doesn't start new processing and isn't charged.

## Send your own OCR

If you already OCR your PDFs, send that OCR with the upload and we read the text from it instead of running our own OCR. It's enabled per account; ask [support@lexselect.io](mailto:support@lexselect.io). Add an `ocr` part and its `ocr_format`:

```sh
gzip -k ./example.textract.json
curl --fail-with-body --silent --show-error \
  -H "Authorization: Bearer $LEXSELECT_API_KEY" \
  -H 'X-API-Version: 2026-11-05' \
  -H "Idempotency-Key: $UPLOAD_OPERATION_ID" \
  -F "name=upload-$UPLOAD_OPERATION_ID.pdf" \
  -F 'ocr_format=aws_textract' \
  -F 'file=@./example.pdf' \
  -F 'ocr=@./example.textract.json.gz' \
  "$LEXSELECT_API_URL/documents/upload"
```

- `aws_textract`: Textract's JSON as returned. Either the `GetDocumentTextDetection` / `GetDocumentAnalysis` result (one response, or the list of its `NextToken` pages), or a list of `DetectDocumentText` / `AnalyzeDocument` responses, one per page in page order.
- `lexselect`: our vendor-neutral `LexSelectOcrV1` format, for any other engine. List each page's words with a box in normalised (0–1), pixel, point or inch units; the [API reference](/api/#tag/files/POST/documents/upload) has the schema.
- PDF only, up to 128 MiB of OCR; gzip it for large documents. With `size`, send `ocr_format` before `file`.
- From Textract's JSON we read only the `WORD` blocks: their `Text`, `Geometry`, `TextType` (handwriting) and `Confidence`. `LINE`, table, form and other blocks are ignored, so you don't need to strip them. An asynchronous result must have `JobStatus` `SUCCEEDED` or `PARTIAL_SUCCESS`.
- Words that run vertically are skipped, as with our own Textract reading. A page whose text runs vertically (for example a landscape table on a page rotated 90°) therefore has no words from your file, and our OCR reads it.
- An invalid file returns `400` with the reason, and no document is created.
- Pages your OCR covers are read from it, so their text is only as good as your OCR engine. Every word you send is kept. A page your file misses, or one with no words, is read by our OCR. Layout, tables and the document tree are built the same way either way.
- The document's `ocr` object reports what was imported, and after processing, `pages_read_from_client` and `pages_read_by_us`.

## Projects, folders and ownership

The root listing contains your projects. Projects contain folders and files. `GET /documents?parent_id=<project-or-folder-id>` lists that parent's direct children. Uploads without `parent_id` go to your Default Project; save its ID from the first upload response's `parent_id`. Create projects and folders in Studio.

`GET /folders/{folder_id}/sub-documents` lists the sub-documents detected in a folder's files: separate documents inside one file, such as the exhibits in a combined PDF. It takes a project or folder ID, not a file ID.

Upload, read results and configure webhooks with the same account.

## Next steps

- [Understand output](/api/understanding-output.html): slices, word coordinates, identifiers, tables and email attachments.
- [Operations and limits](/api/operations.html): errors, retries, idempotency, billing, retention and limits.
- [Webhooks](/api/webhooks.html): receive and verify processing events.
- [Versions and changelog](/api/versioning.html): what each API version changes.
- [Examples](https://github.com/LexSelect/lexselect-api-demo): server-side examples and a CLI.

Questions? Email [support@lexselect.io](mailto:support@lexselect.io) with the request ID (`X-Request-Id`) or document ID.
