A robust Python SDK for integrating with Exact Online APIs
Project description
exact-online-sdk
A robust, production-ready Python SDK for integrating with Exact Online APIs. It emphasizes reliability, type safety, and ergonomics for finance, CRM, logistics, and inventory workflows.
Features
- OAuth2 built-in – automatic token acquisition/refresh with pluggable encrypted storage.
- Strict Pydantic v2 models – 210+ model files with request/response validation mirroring Exact payload schemas.
- Sync + Async clients – powered by
httpx, sharing the same retry, timeout, and pagination semantics. - Retries, rate limiting, and request IDs – exponential backoff,
Retry-Afterhonoring, and rich exception hierarchy with Exact request identifiers for support tickets. - 270 endpoint classes – CRUD helpers, typed pagination, and
$filter/$selectbuilders covering all 270 tracked Exact Online API entities across accountancy, activities, assets, budget, bulk, cashflow, CRM, documents, financial, general, HRM, inventory, logistics, mailbox, manufacturing, payroll, project, purchase, quotation, sales, subscription, system, users, VAT, webhooks, and sync endpoints. - Structured logging – optional JSON output with automatic redaction of sensitive fields.
- AWS Secrets Manager integration – optional, thread-safe token storage via
boto3(pip install exact-online-sdk[aws]). - 100 % test coverage – 738 tests enforced by CI (
--cov-fail-under=100).
Installation
# Development install (with tooling extras)
uv pip install -e .[dev]
# Production / consumer install
uv pip install exact-online-sdk
# With AWS Secrets Manager support
uv pip install exact-online-sdk[aws]
Configuration
Set environment variables or a .env file in project root:
EXACT_CLIENT_ID=your-client-id
EXACT_CLIENT_SECRET=your-client-secret
EXACT_REDIRECT_URI=https://your-app.example/callback
EXACT_BASE_URL=https://start.exactonline.nl
# Optional
EXACT_ENCRYPTION_KEY=base64url_fernet_key
EXACT_TOKEN_PATH=~/.exact_online_token.json.enc
EXACT_LOG_LEVEL=INFO
EXACT_LOG_JSON=false
EXACT_LOG_FILE=
EXACT_TIMEOUT=30
EXACT_USER_AGENT=exact-online-sdk/1.0.6 (+https://github.com/carlospaiva/exact-online-sdk)
The SDK derives default auth/token URLs from EXACT_BASE_URL (/api/oauth2/auth and /api/oauth2/token).
Bootstrap Authentication
For first-time setup or local development, use scripts/bootstrap_auth.py to acquire and persist an OAuth token. This script supports two modes: manual callback URL paste, or a temporary localhost listener.
Required environment variables
Add these to your .env file:
EXACT_CLIENT_ID=your-client-id
EXACT_CLIENT_SECRET=your-client-secret
EXACT_REDIRECT_URI=https://localhost:8000/callback
EXACT_ENCRYPTION_KEY=your-fernet-key
EXACT_TOKEN_PATH=~/.exact_online_token.json.enc
Security note: Never commit .env files or hardcode secrets in source code. Add .env to your .gitignore.
Generate a Fernet encryption key
Token persistence uses Fernet symmetric encryption. Generate a key:
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
Copy the output and set it as EXACT_ENCRYPTION_KEY in your .env.
Manual mode
Prints the authorization URL and waits for you to paste the callback URL:
uv run python scripts/bootstrap_auth.py --mode manual
- Open the printed URL in your browser
- Authenticate with Exact Online
- Copy the full callback URL from your browser
- Paste it into the terminal prompt
Listener mode
Starts a temporary HTTPS localhost server to capture the callback automatically. When using listener mode, set both TLS file paths in your environment:
EXACT_LISTENER_CERT_PATH=/absolute/path/to/listener-cert.pem
EXACT_LISTENER_KEY_PATH=/absolute/path/to/listener-key.pem
The EXACT_REDIRECT_URI value must use the https scheme for listener mode.
uv run python scripts/bootstrap_auth.py --mode listener
- Open the printed URL in your browser
- Authenticate with Exact Online
- The browser redirects to localhost, the script captures the code, and the token is persisted
Token persistence requirements
Tokens are only persisted when both EXACT_ENCRYPTION_KEY and EXACT_TOKEN_PATH are set. If either is missing, the bootstrap will fail fast with an error message indicating the missing configuration.
Next steps after bootstrap
Once authenticated, you can use scripts/fetch_accounts.py to verify the connection. Note that this script still requires EXACT_DIVISION (set via environment variable or --division flag):
# Set division via environment
export EXACT_DIVISION=123456
uv run python scripts/fetch_accounts.py
# Or pass as argument
uv run python scripts/fetch_accounts.py --division 123456
Important: bootstrap_auth.py does not require EXACT_DIVISION, but fetch_accounts.py does.
from exact_online_sdk import ExactOnlineClient, Settings
settings = Settings.from_env()
client = ExactOnlineClient(settings=settings)
# List accounts (first page)
accounts = client.get("crm/Accounts", params={"$top": 10})
print(accounts)
Working with typed endpoints
from exact_online_sdk.api.endpoints import ContactsAPI
from exact_online_sdk.models import Contact
contacts_api = ContactsAPI(client)
# Create using strict model validation
contact = Contact(first_name="Sam", last_name="Taylor", email="sam@example.com")
created = contacts_api.create(contact)
# Filter & paginate using OData helpers
results = contacts_api.list(
filters={"Email": ("eq", "sam@example.com")},
select=["ID", "FullName", "Email"],
top=25,
)
for page in contacts_api.iter_pages(page_size=100):
...
AWS Secrets Manager token storage
from exact_online_sdk import ExactOnlineClient, Settings
from exact_online_sdk.auth import ExactOnlineAuth
from exact_online_sdk.contrib.aws import SecretsManagerTokenStorage
settings = Settings.from_env()
storage = SecretsManagerTokenStorage(
"my-app/exact-online/oauth-token",
region_name="eu-west-1",
)
auth = ExactOnlineAuth(settings, storage=storage)
client = ExactOnlineClient(settings=settings, auth=auth)
# Tokens are now persisted in AWS Secrets Manager.
# Safe for multi-threaded and serverless (Lambda) use.
accounts = client.get("crm/Accounts")
Read-only mode for multi-container deployments
When multiple containers share a token (e.g. Prefect on ECS), use ReadOnlyTokenStorage in workers so only a dedicated Lambda refreshes the token:
from exact_online_sdk import ExactOnlineClient, Settings
from exact_online_sdk.auth import ExactOnlineAuth
from exact_online_sdk.contrib.aws import ReadOnlyTokenStorage, SecretsManagerTokenStorage
settings = Settings.from_env()
inner = SecretsManagerTokenStorage("my-app/exact-online/oauth-token")
storage = ReadOnlyTokenStorage(inner)
auth = ExactOnlineAuth(settings, storage=storage)
client = ExactOnlineClient(settings=settings, auth=auth)
# Reads work normally; any accidental refresh attempt raises AuthenticationError.
accounts = client.get("crm/Accounts")
Error handling with context
from exact_online_sdk.exceptions import APIError, AuthenticationError
try:
client.get("crm/Accounts")
except AuthenticationError as exc:
logger.error("Auth failed: %s", exc)
except APIError as exc:
logger.error(
"Exact API error", status=exc.status_code, request_id=exc.context.get("request_id")
)
Response format handling
The SDK explicitly requests JSON responses from the Exact Online API by sending an Accept: application/json header on all API calls. This ensures consistent behavior and prevents the API from returning Atom/XML or other formats that the SDK does not parse.
If you need to override the response format for a specific request, you can pass a $format parameter. The SDK preserves explicit caller overrides and will not overwrite your preference:
# The SDK sends Accept: application/json by default
accounts = client.get("crm/Accounts")
# Explicit format override is preserved
accounts = client.get("crm/Accounts", params={"$format": "xml"})
When the API returns a non-JSON success response (2xx status with unexpected content type), the SDK raises a descriptive APIError with context about the actual content type and URL. This replaces raw JSON parser failures with actionable error messages that help diagnose API behavior changes or misconfigurations.
Regression tests for JSON negotiation and non-JSON failure handling live in tests/client/unit/ and tests/auth/unit/.
Development & Testing
All commands are run through uv to ensure consistent virtual environments:
| Task | Command |
|---|---|
| Install dependencies | uv sync |
| Format | uv run black . && uv run isort . |
| Lint | uv run flake8 && uv run pylint src tests |
| Type check | uv run mypy src tests |
| Unit/Integration tests | uv run pytest (or target subfolders) |
| Coverage report | uv run pytest --cov=src --cov-report=term-missing |
Release Workflow
- Bump the version in
pyproject.toml(SemVer). - Run formatting, lint, type-check, and full test suite (see table above).
- Build artifacts:
uv build(wheel + sdist land indist/). - Smoke-test the wheel:
uv pip install dist/exact_online_sdk-<version>-py3-none-any.whlin a clean env. - Publish when ready:
uv publish(requires configured PyPI token). - Tag the release (
git tag -a vX.Y.Z -m "Release X.Y.Z") and push tags.
Contributing & Support
- See AGENTS.md for detailed engineering guidelines, security posture, and release expectations.
- Issues and PRs are welcome—please include tests and documentation for any behavior change.
License
MIT License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file exact_online_sdk-1.0.6.tar.gz.
File metadata
- Download URL: exact_online_sdk-1.0.6.tar.gz
- Upload date:
- Size: 83.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
06b9d4fbcc6b7596ef480eeb2d10dca9352e916950fb4da07188f54eaaf6215e
|
|
| MD5 |
c23246598aa90d711d6b269176bc0f07
|
|
| BLAKE2b-256 |
6b84754accf6b27f2555efce86acd1b01972bfd3c071657a4631eb5e63101bc0
|
File details
Details for the file exact_online_sdk-1.0.6-py3-none-any.whl.
File metadata
- Download URL: exact_online_sdk-1.0.6-py3-none-any.whl
- Upload date:
- Size: 177.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2b270e69f026328fafa18596be426ffe560ae4e1a7b9a8e93de3c8847ad37c3d
|
|
| MD5 |
2112773a57b9f416174996c0181a21d8
|
|
| BLAKE2b-256 |
04dfa18958380194443f978fec86337b96f2d0325ddd4ea83ee6f545b2d8b0e5
|