Metadata-Version: 2.4
Name: flex-api
Version: 1.0.0b35
Summary: Client for the Flex on the Job tenant API (/api/v1): inventory, jobs, invoices, purchasing.
License: Proprietary. See LICENSE file.
Project-URL: Homepage, https://flexonthejob.com/developers
Project-URL: API reference, https://app.flexonthejob.com/docs
Keywords: flex,inventory,field-service,openapi,api-client
Classifier: License :: Other/Proprietary License
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# flex-api (Python)

Client for the [Flex on the Job](https://flexonthejob.com) tenant API. Standard library only, Python 3.9+.

```python
from flex_api import FlexClient, FlexApiError, SANDBOX_URL

flex = FlexClient(api_key="flx_live_...")          # Settings -> Integrations -> API Keys
# flex = FlexClient("flx_live_YLJG6oxA6hOiJTT7ziyAOFs8nJAJfbRE", base_url=SANDBOX_URL)  # public read-only demo

for item in flex.paginate("/api/v1/items", search="filter"):
    print(item["name"], item["onHand"])

# Writes get an Idempotency-Key automatically (kept across retries).
flex.post("/api/v1/items/stock/batch", {"operations": [
    {"action": "Adjust", "itemId": 12, "locationId": 3, "delta": -2, "reason": "Cycle count"},
    {"action": "Move", "itemId": 12, "fromLocationId": 3, "toLocationId": 4, "quantity": 1},
]})

try:
    flex.post("/api/v1/purchase-orders", {"receivingLocationId": 1, "lines": [{"itemId": 999, "quantityOrdered": 1}]})
except FlexApiError as e:
    print(e.status, e.detail, e.errors)   # 422 ... {'lines[0].itemId': [...]}
```

What the client does for you:

- `Authorization: Bearer` on every request.
- An `Idempotency-Key` on every POST/PUT/PATCH/DELETE (pass `idempotency_key=` to choose your own), reused if the
  request is retried, so a retry is never applied twice.
- Retries 429 and 503 (default 3 times), waiting `Retry-After` (seconds or an HTTP date), and a 409 "still being
  processed" while an earlier attempt with the same key is running.
- `flex.last_response_headers` holds the last response's headers: send its `etag` back as `if_match=` on
  `put()` / `patch()` to refuse an update if the record changed in between (412).
- `paginate(path, **query)` follows `nextCursor` across pages.
- Errors raise `FlexApiError` with the RFC 7807 problem: `status`, `detail`, and `errors` per field.

Paths, parameters and bodies are exactly those of the OpenAPI document at
<https://app.flexonthejob.com/openapi/v1.json> (browsable at `/docs`). Tests: `python -m unittest discover -s tests`.

Examples in `examples/`: `cycle_count.py`, `invoice_completed_jobs.py` (`--finalize` also marks the drafts sent), `receive_delivery.py` and `reorder_low_stock.py`. They talk to the sandbox unless you set `FLEX_BASE_URL=https://app.flexonthejob.com`. The last three preview
their change and write only after `--apply`, `--yes` or an interactive yes. The Flex MCP server (`/mcp`) offers the same tasks as prompts (`cycle_count`, `invoice_completed_jobs`, `receive_delivery`, `reorder_below_minimum`).

## License

Proprietary — Copyright (c) 2026 Flex Services, LLC. All rights reserved. Licensed for use in your
applications to access the Flex on the Job API; you may not copy, modify, redistribute, or reverse-engineer
it. See the [LICENSE](./LICENSE) file for the full terms.
