Skip to main content

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-After honoring, and rich exception hierarchy with Exact request identifiers for support tickets.
  • 270 endpoint classes – CRUD helpers, typed pagination, and $filter/$select builders 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
  1. Open the printed URL in your browser
  2. Authenticate with Exact Online
  3. Copy the full callback URL from your browser
  4. 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
  1. Open the printed URL in your browser
  2. Authenticate with Exact Online
  3. 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

  1. Bump the version in pyproject.toml (SemVer).
  2. Run formatting, lint, type-check, and full test suite (see table above).
  3. Build artifacts: uv build (wheel + sdist land in dist/).
  4. Smoke-test the wheel: uv pip install dist/exact_online_sdk-<version>-py3-none-any.whl in a clean env.
  5. Publish when ready: uv publish (requires configured PyPI token).
  6. 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

exact_online_sdk-1.0.6.tar.gz (83.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

exact_online_sdk-1.0.6-py3-none-any.whl (177.8 kB view details)

Uploaded Python 3

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

Hashes for exact_online_sdk-1.0.6.tar.gz
Algorithm Hash digest
SHA256 06b9d4fbcc6b7596ef480eeb2d10dca9352e916950fb4da07188f54eaaf6215e
MD5 c23246598aa90d711d6b269176bc0f07
BLAKE2b-256 6b84754accf6b27f2555efce86acd1b01972bfd3c071657a4631eb5e63101bc0

See more details on using hashes here.

File details

Details for the file exact_online_sdk-1.0.6-py3-none-any.whl.

File metadata

File hashes

Hashes for exact_online_sdk-1.0.6-py3-none-any.whl
Algorithm Hash digest
SHA256 2b270e69f026328fafa18596be426ffe560ae4e1a7b9a8e93de3c8847ad37c3d
MD5 2112773a57b9f416174996c0181a21d8
BLAKE2b-256 04dfa18958380194443f978fec86337b96f2d0325ddd4ea83ee6f545b2d8b0e5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page