> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.emtelligent.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.emtelligent.com/_mcp/server.

# Quickstart Code Example

The Prep API works **bucket to bucket**: you tell it where a document already
lives in your cloud storage and where to write the output, and it reads and
writes directly. Nothing is uploaded through the API, so this example needs a
little more setup than a plain file post — you supply storage credentials
alongside your own.

## Create a client

The API accepts either of two credentials, and `get_auth_session` returns the
right kind of session for whichever you pass. Two things hold for both: the API
URL is the **first positional argument** — it is what the issued token is scoped
to — and `credentials` is a **dictionary**.

> **The API URL is yours, not a shared one**
>
> Prep is not served from a public endpoint. Emtelligent supplies the URL for your
> deployment along with your credentials; `<your-prep-endpoint>` below stands in
> for it.

Each tab below is complete on its own: the storage credentials are the same for
both, your own are not.

#### API key

An API key is sent in the `x-api-key` header. Create one with the `prep:process`
scope on the [emtelligent platform](/platform/api-keys).

```bash
pip install emtellisplit-sdk-python

export EMTELLISPLIT_API_KEY='...'

# read your input bucket and write your output bucket
export AWS_ACCESS_KEY_ID='...'
export AWS_SECRET_ACCESS_KEY='...'
```

```python
import os

from emtellisplit_sdk_python import auth, engine

SERVER = "https://<your-prep-endpoint>"

session = auth.get_auth_session(
    SERVER, credentials={"api_key": os.environ["EMTELLISPLIT_API_KEY"]})
client = engine.Emtellisplit(SERVER, session)
```

#### IAM username and password

Your IAM user works too. The session exchanges your username and password for a
token at IAM's `/oauth/token` endpoint — an OAuth resource-owner-password grant,
scoped to the API URL you pass as the audience — sends it as a bearer token, and
gets a fresh one by itself on any 401 or 403. A long-running job does not need you
to handle expiry.

```bash
pip install emtellisplit-sdk-python

export EMTELLIGENT_IAM_URL='https://iam.us.emtelligent.com'
export EMTELLIGENT_USERNAME='...'
# single quotes so the shell does not interpret characters in the password
export EMTELLIGENT_PASSWORD='...'

# read your input bucket and write your output bucket
export AWS_ACCESS_KEY_ID='...'
export AWS_SECRET_ACCESS_KEY='...'
```

```python
import os

from emtellisplit_sdk_python import auth, engine

SERVER = "https://<your-prep-endpoint>"

session = auth.get_auth_session(
    SERVER,
    iam_url=os.environ["EMTELLIGENT_IAM_URL"],
    credentials={
        "username": os.environ["EMTELLIGENT_USERNAME"],
        "password": os.environ["EMTELLIGENT_PASSWORD"],
    },
)
client = engine.Emtellisplit(SERVER, session)
```

> **Note**
>
> `SERVER` is passed twice on purpose. `get_auth_session` sends it as the token's
> audience, so the token IAM issues is only valid for that API; `Emtellisplit` uses
> it as the address to call. They have to match.
>
> The dictionary key is `username`. The token request IAM receives uses `user_name`,
> and so does the error message the SDK raises when a credential set matches
> neither scheme — but `user_name` is not the key to pass here.
>
> On-premises installations run their own IAM, so `iam_url` is the host for your
> deployment rather than `iam.us.emtelligent.com`, and it may be `http`. Give the
> host only; the SDK appends the endpoint.

> **Note**
>
> Needs SDK **1.2.2 or newer**, which added the API URL argument and moved the login
> to the shared IAM client. On earlier versions this raises
> `TypeError: get_auth_session() missing 1 required positional argument` — drop the
> first argument and it works, or upgrade with
> `pip install -U emtellisplit-sdk-python`.

## Describe the job

`InputDocument` names the object to process, the buckets to read and write, and
the credentials the service should use for them. `operation='split-ocr-ep'` runs
the full set of functions — split, classify, OCR and metadata extraction.

The keys in `cloud_credential_set` are passed straight through to the cloud
client, so they are that provider's own parameter names — `aws_` prefix included.
For S3 they are `aws_access_key_id` and `aws_secret_access_key`, plus
`aws_session_token` if you are using temporary credentials. For Azure, either
`connection_string`, or `account_name` with one of `account_primary_access_key` or
`sas_token`. Anything else comes back as `Invalid cred format for s3`.

```python
from emtellisplit_sdk_python.data import InputDocument

doc = InputDocument(
    cloud_provider="s3",
    cloud_credential_set={
        "aws_access_key_id": os.environ["AWS_ACCESS_KEY_ID"],
        "aws_secret_access_key": os.environ["AWS_SECRET_ACCESS_KEY"],
    },
    input_object_keys={"document": "subdir/my_document.pdf"},
    input_bucket="my-input-bucket",
    output_bucket="my-output-bucket",
    output_bucket_prefix="my-jobs",
    operation="split-ocr-ep",
)
```

> **Note**
>
> Output is written to `<output_bucket_prefix>/<job_id>/`, and `get_result` looks
> under exactly that path. Pass the prefix rather than leaving it out: SDK versions
> up to 1.1.3 default it to the literal string `unused`, so output arrives under a
> folder nobody chose — and those versions ignore the prefix when fetching, so
> `get_result` returns nothing whatever you pass. If it comes back empty, check your
> version with `pip show emtellisplit-sdk-python`; the objects are in the bucket
> either way.

> **Warning**
>
> These credentials are used by the service to read your input bucket and write
> your output bucket. Scope them to exactly those two buckets rather than reusing
> a broad key, and prefer temporary credentials — pass the `aws_session_token`
> they come with alongside the key and secret.

## Submit and wait

`process` returns a `ResultFuture`, and **it has to be awaited** — it polls the
job on an asyncio task, so it only works inside async code. Both spellings below
do the same thing.

```python
result = await client.process(doc)        # the future is awaitable itself
result = await client.process(doc).result()   # or await its result()
```

Awaiting gives you a `CloudResult` if the job succeeded and a plain `Result` if it
failed or was cancelled, so `isinstance` is the success test.

```python
from emtellisplit_sdk_python.data import CloudResult

result = await client.process(doc)
if not isinstance(result, CloudResult):
    raise SystemExit(f"processing did not succeed: {result.job_status.status}")
```

Prefer to stay synchronous? `submit` returns a `DeferredResult` instead, which is
deliberately not awaitable — you poll it yourself. A `JobStatus` is truthy once the
job reaches a terminal state.

```python
import time

deferred = client.submit(doc)
while not deferred.done():
    time.sleep(10)
result = deferred.result()
```

## Fetch the output

The split documents are in your output bucket, and `get_result` reads them back.
These credentials are **not** sent to the service — this call is your own client
listing and reading your own bucket — so they only need list and read on it.

```python
output = result.get_result(
    "s3",
    "my-output-bucket",
    {
        "aws_access_key_id": os.environ["AWS_ACCESS_KEY_ID"],
        "aws_secret_access_key": os.environ["AWS_SECRET_ACCESS_KEY"],
    },
)
if not output.docs:
    raise SystemExit(f"job {result.job_id} succeeded but no output was found")
for doc_result in output.docs:
    print(doc_result.emtellipro_category, doc_result.emtellipro_subcategory,
          doc_result.page_span)
```

> **Warning**
>
> Check `output.docs` rather than looping straight over it. `get_result` returns an
> empty collection when it finds nothing in the bucket — it does not raise — so a
> bare loop over it prints nothing and looks like a job that produced no documents.

One bundle in gives many documents out — that is the split — each with the
category and subcategory the classifier assigned and the pages it came from.

## The whole thing

Only the session differs between the two, so pick the tab you set up above.

#### API key

**`prep_quickstart.py`**

```python title="prep_quickstart.py"
import asyncio
import os

from emtellisplit_sdk_python import auth, engine
from emtellisplit_sdk_python.data import CloudResult, InputDocument

SERVER = "https://<your-prep-endpoint>"
INPUT_BUCKET = "my-input-bucket"
OUTPUT_BUCKET = "my-output-bucket"
OUTPUT_PREFIX = "my-jobs"
MY_STORAGE = {
    "aws_access_key_id": os.environ["AWS_ACCESS_KEY_ID"],
    "aws_secret_access_key": os.environ["AWS_SECRET_ACCESS_KEY"],
}


async def main():
    session = auth.get_auth_session(
        SERVER, credentials={"api_key": os.environ["EMTELLISPLIT_API_KEY"]})
    client = engine.Emtellisplit(SERVER, session)

    doc = InputDocument(
        cloud_provider="s3",
        cloud_credential_set=MY_STORAGE,
        input_object_keys={"document": "subdir/my_document.pdf"},
        input_bucket=INPUT_BUCKET,
        output_bucket=OUTPUT_BUCKET,
        output_bucket_prefix=OUTPUT_PREFIX,
        operation="split-ocr-ep",
    )

    # polls every 10 seconds until the job reaches a terminal state
    result = await client.process(doc)
    if not isinstance(result, CloudResult):
        raise SystemExit(f"processing did not succeed: {result.job_status.status}")

    output = result.get_result("s3", OUTPUT_BUCKET, MY_STORAGE)
    if not output.docs:
        raise SystemExit(f"job {result.job_id} succeeded but no output was found "
                         f"in {OUTPUT_BUCKET}")
    for doc_result in output.docs:
        print(doc_result.emtellipro_category, doc_result.emtellipro_subcategory,
              doc_result.page_span)


asyncio.run(main())
```

#### IAM username and password

**`prep_quickstart.py`**

```python title="prep_quickstart.py"
import asyncio
import os

from emtellisplit_sdk_python import auth, engine
from emtellisplit_sdk_python.data import CloudResult, InputDocument

SERVER = "https://<your-prep-endpoint>"
INPUT_BUCKET = "my-input-bucket"
OUTPUT_BUCKET = "my-output-bucket"
OUTPUT_PREFIX = "my-jobs"
MY_STORAGE = {
    "aws_access_key_id": os.environ["AWS_ACCESS_KEY_ID"],
    "aws_secret_access_key": os.environ["AWS_SECRET_ACCESS_KEY"],
}


async def main():
    session = auth.get_auth_session(
        SERVER,
        iam_url=os.environ["EMTELLIGENT_IAM_URL"],
        credentials={
            "username": os.environ["EMTELLIGENT_USERNAME"],
            "password": os.environ["EMTELLIGENT_PASSWORD"],
        },
    )
    client = engine.Emtellisplit(SERVER, session)

    doc = InputDocument(
        cloud_provider="s3",
        cloud_credential_set=MY_STORAGE,
        input_object_keys={"document": "subdir/my_document.pdf"},
        input_bucket=INPUT_BUCKET,
        output_bucket=OUTPUT_BUCKET,
        output_bucket_prefix=OUTPUT_PREFIX,
        operation="split-ocr-ep",
    )

    # polls every 10 seconds until the job reaches a terminal state
    result = await client.process(doc)
    if not isinstance(result, CloudResult):
        raise SystemExit(f"processing did not succeed: {result.job_status.status}")

    output = result.get_result("s3", OUTPUT_BUCKET, MY_STORAGE)
    if not output.docs:
        raise SystemExit(f"job {result.job_id} succeeded but no output was found "
                         f"in {OUTPUT_BUCKET}")
    for doc_result in output.docs:
        print(doc_result.emtellipro_category, doc_result.emtellipro_subcategory,
              doc_result.page_span)


asyncio.run(main())
```

If `get_result` finds nothing, the output is still there — list your bucket under
`<output_bucket_prefix>/<job_id>/` and you will see it. That gap between where the
service writes and where the SDK looks is the version difference described above,
not a job that produced nothing.

## Next

#### [API Reference](/prep/api)

The submit, status and cancel operations in full.

#### [Command-line client](/prep/client/overview)

The same job without writing any Python.