Metadata-Version: 2.4
Name: indox-client
Version: 0.8.0
Summary: Python SDK for the Indox media and document conversion services.
Author-email: Indox <support@indox.org>
License-Expression: LicenseRef-Proprietary
Project-URL: Homepage, https://indox.org
Project-URL: Source, https://github.com/nematiai/indox-sdk
Project-URL: Issues, https://github.com/nematiai/indox-sdk/issues
Keywords: indox,document-conversion,media-conversion,sdk,api-client
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.28.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: responses>=0.23.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Dynamic: license-file

# indox-client

Python SDK for the Indox API v1 — document, image, video, 3D-model and font conversion.

**Requires:** Python ≥ 3.9 · `requests ≥ 2.28`

## Install

```bash
pip install indox-client
```

## Authenticate

Create an API key in your Indox dashboard under **API Keys**. The value is shown once at
creation and starts with `ak_`.

Pass it directly, or set `INDOX_API_KEY` and let the client pick it up:

```bash
export INDOX_API_KEY='ak_your_key_here'
```

```python
from indox_client import Indox

with Indox() as client:                 # reads INDOX_API_KEY
    print(client.health.get())

with Indox(api_key="ak_...") as client:  # or pass it explicitly
    ...
```

The client defaults to `https://indox.org`. Point it elsewhere only if you run your own
instance — `Indox(base_url="https://api.example.com")`, or set `INDOX_BASE_URL`.

## Convert a file

Conversions are asynchronous: submit, poll, download.

```python
from indox_client import Indox

with Indox() as client:
    job = client.images.convert("photo.png", target_format="webp")
    done = client.conversion.wait(job["id"], timeout=120)   # works for any converter
    client.media.download(job["id"], "photo.webp")
```

Fonts have a one-call helper that does all three steps:

```python
with Indox() as client:
    client.fonts.convert_and_download(
        "MyFont.ttf", target_format="woff2", output_path="MyFont.woff2"
    )
```

Before spending credits, you can check a conversion is supported:

```python
client.fonts.validate("MyFont.ttf", target_format="woff2")
# {'valid': True, 'input': 'ttf', 'output': 'woff2',
#  'engine': 'fonttools', 'credits': 1, 'error': None}
```

## Discover what is supported

```python
client.images.formats()        # image formats and pairs
client.videos.formats()
client.models.formats()
client.pdf.formats()
client.fonts.formats.list()
client.fonts.formats.get("ttf")
```

## Resources

| Attribute | Covers |
|---|---|
| `client.health` | service health |
| `client.images` · `client.videos` · `client.models` | media conversion |
| `client.media` | media job status, downloads, batches, credits |
| `client.conversion` | one `get` / `wait` / `delete` for any conversion id, whichever converter made it |
| `client.pdf` | document conversion, retry, keep policy, save to files, email, form fields, pipelines, phone scan, e-signing |
| `client.pdf.forms` | public IRS/USCIS blank forms: list, fields, blank download URL, fill |
| `client.pdf.ai` | ask, fill, detect fields, suggest comments, check numbers, classify, edit (credits; poll `job`) |
| `client.docs` | document job status, batches, credits |
| `client.fonts` | formats, quota, upload, convert (with optional subsetting), validate, inspect, download |
| `client.deck` | AI slide decks: `templates` (and brands), `sources`, `images`, `projects`, `research`, `authoring`, `quality`, `edit`, `fill`, `from_image`, `narration`, `documents` |
| `client.office` | office documents (sheet/doc/slide), import/export jobs and downloads |
| `client.webhooks` | create, list, update, delete, deliveries, test, rotate secret |
| `client.user` | login/2FA/refresh, profile, avatar, branding, integrations, preferences, storage (incl. presigned uploads), usage, share links |
| `client.notifications` | in-app notifications and email notification settings |
| `client.storage` | connected Box / Dropbox / Google Drive / OneDrive: `status(provider)`, `contents(provider)` |
| `client.audit` | your audit log: list, CSV export |
| `client.billing` | credit balance, low-balance thresholds, rate card, current subscription, usage by feature, plans, purchases |

Operations that start AI or paid work (`deck` stages, authoring, research, narration,
`pdf.ai`, `office` import/export) spend credits; poll their job endpoints for the result.

## Signing in (account operations)

API keys convert files; they cannot manage the account. These six calls answer 403 to an API
key and need a sign-in access token instead: `user.update_me`, `user.create_share`,
`user.delete_share`, `billing.purchase_initiate`, `billing.purchase_pay_as_you_go`,
`billing.storage_initiate`. Pass it per call with `access_token=`:

```python
from indox_client import Indox, MfaRequired

with Indox() as client:
    try:
        session = client.user.login(email="me@example.com", password="...")
    except MfaRequired as challenge:
        session = client.user.complete_2fa(challenge.mfa_token, input("2FA code: "))
    token = session["external_token"]
    client.user.update_me(first_name="Ada", access_token=token)
```

`client.user.refresh()` returns a new `external_token`. Call it on the same client that logged
in: the refresh token comes back as a cookie, not in the body. Login takes a JSON body only.

## Errors, retries and timeouts

Failures raise typed exceptions, all importable from `indox_client` and all
subclasses of `IndoxError`.

Errors carrying an HTTP response expose `status_code` and the parsed `response`:

| Exception | When |
|---|---|
| `BadRequestError` | 400, and 422 validation errors |
| `AuthenticationError` | 401; its subclass `MfaRequired` (with `mfa_token`) when login needs a 2FA code |
| `PaymentRequiredError` | 402, not enough credits |
| `PermissionDeniedError` | 403; its subclass `ScopeError` when the API key lacks the scope (`auth.api_key_scope_missing`) |
| `NotFoundError` | 404, including ids you do not own |
| `GoneError` | 410, an expired output or signing link |
| `RateLimitError` | 429 after the retries below ran out; `retry_after` holds the server hint in seconds |
| `InternalServerError` | 5xx |
| `APIStatusError` | any other status (e.g. 409) |

The message comes from the body's `detail` (a string, or a list of validation errors
rendered as `body.field: reason`), falling back to `error`:

```python
from indox_client import Indox, BadRequestError

try:
    client.images.convert("photo.png", target_format="nope")
except BadRequestError as exc:
    print(exc.status_code, exc.message, exc.response)
```

`APIConnectionError` (the request never reached the API) and `ConversionError` (a job
failed server-side) have no HTTP status. Catch `IndoxError` to cover everything.

Every endpoint is rate limited. The client retries for you:

- **429** on any method: waits `Retry-After` (seconds or an HTTP date), otherwise exponential
  backoff with jitter, then retries.
- **502 / 503 / 504** on `GET` only; other methods are never replayed.
- At most 3 retries, and no single wait longer than 60 s. Then the error is raised.

Tune it per client (defaults are the module constants `DEFAULT_MAX_RETRIES`,
`DEFAULT_MAX_RETRY_WAIT`, `DEFAULT_RETRY_BACKOFF`); `max_retries=0` turns retrying off:

```python
Indox(max_retries=5, max_retry_wait=30.0, retry_backoff=1.0)
```

Requests use a 5 s connect / 60 s read timeout by default:
`Indox(timeout=(5.0, 120.0))` to change it.

## Upgrading from 0.7

- **Breaking:** `pdf.run_pipeline(pipeline_id, file_path=None, *, data=None)` uploads the PDF as
  multipart. It used to take `payload` and send JSON with no file, which the server could not run.
  Pass the PDF path, and any extra form fields in `data`.
- New: a typed `client.pdf` method for each of the 50 convert-based PDF tools, e.g.
  `client.pdf.flatten("in.pdf", flatten=True)`, plus `run_tool`, `list_tools` and
  `describe_tool` over the packaged `pdf-tools.json` catalog.

## Upgrading from 0.5

- `user.update_me(...)` takes `username`, `first_name`, `last_name` keywords; `email` moved to
  `user.change_email(email, current_password=...)`.
- `user.upload_file(path, related_task_id=None)`: `s3_key` and `storage_already_tracked` are gone.
- `images` / `videos` / `models` `.convert`: `user_id` was removed; passing it in `data` raises
  `ValueError`.
- `pdf.submit_sign(token, otp=None, *, consent, name=None, date=None, signature=None)`:
  `consent` is required; pass the emailed code from `pdf.request_sign_otp(token)` when
  `pdf.get_sign_token(token)` reports `require_code`. Sessions return `signed_url` (was `signed_s3_key`), the
  sign view returns `document_url` (was `download_key`), and participants no longer carry
  `sign_path`.
- `pdf.create_signing_session(path, emails=[...], title=None)` uploads the PDF (multipart).
- `docs.batch_collect(ids)` and `docs.hide(ids)` take a list of UUIDs.
- `billing.task_completion`: the server ignores `credit_cost` and the `output_*` fields; send
  `task_type`.
- `client.fonts.health()` and `client.fonts.quota()` return the new shapes
  (`engines`, `storage`, `max_upload_mb`, …).
- A convert started with a `redirect_url` returns a `callback_signing_key`; the callback is
  signed with it under the event `conversion.callback`.
- Waits now return a `partial` conversion (some formats succeeded) instead of timing out.

## Links

- Source, docs, and SDKs for 7 other languages — <https://github.com/nematiai/indox-sdk>
- Indox — <https://indox.org>
