Jobber Python Client
Minimal Python client for Jobber GraphQL API with OAuth 2.0 support.
Features
- One-time authentication: Browser-based OAuth flow stores tokens in Doppler
- Visual confirmation URLs: Get web links to verify API operations in Jobber UI (Quick Win Guide)
- Fail-fast errors: All failures raise exceptions with context
- Token auto-refresh: Transparent token refresh before expiration
- Rate limit awareness: Exposes throttle status, raises before exceeding limits
- Minimal dependencies: Only
requestsandoauthlibrequired
Installation
# Install from PyPI
pip install jobber-python-client
# Or clone repository for development
git clone https://github.com/tainora/jobber-python-client.git
cd jobber-python-client
uv sync
Prerequisites
- Jobber Developer Account: Create app at https://developer.getjobber.com/
- Doppler CLI: Install from https://docs.doppler.com/docs/install-cli
- OAuth Credentials: Store in Doppler:
doppler secrets set JOBBER_CLIENT_ID="your_client_id" \ JOBBER_CLIENT_SECRET="your_client_secret" \ --project claude-config --config dev
Quick Start
Step 1: Authenticate (one-time)
uv run jobber_auth.py
This will:
- Open browser for Jobber authorization
- Exchange authorization code for tokens
- Store tokens in Doppler
Step 2: Use in AI agent code
from jobber import JobberClient
# Create client (loads credentials from Doppler)
client = JobberClient.from_doppler()
# Execute GraphQL queries
result = client.execute_query("""
query {
clients(first: 10) {
nodes {
id
firstName
lastName
}
totalCount
}
}
""")
print(f"Total clients: {result['clients']['totalCount']}")
Visual Confirmation URLs (Quick Win!)
Problem: API operations feel abstract. You create a client via API, but can you see it in Jobber's web interface?
Solution: YES! Include jobberWebUri in your queries to get clickable web links.
Example: Create Client with Web Link
from jobber import JobberClient
client = JobberClient.from_doppler()
# IMPORTANT: Include jobberWebUri in mutation response
mutation = """
mutation CreateClient($input: ClientCreate!) {
clientCreate(input: $input) {
client {
id
firstName
lastName
jobberWebUri # ← Returns web URL!
}
}
}
"""
result = client.execute_query(mutation, {
'input': {
'firstName': 'John',
'lastName': 'Doe'
}
})
created = result['clientCreate']['client']
# Show clickable link for visual confirmation
print(f"✅ Client created: {created['firstName']} {created['lastName']}")
print(f"🔗 View in Jobber: {created['jobberWebUri']}")
# Click link → See client in Jobber web UI → Instant verification!
Available URL Fields
| Field | Available On | Purpose |
|---|---|---|
jobberWebUri |
Most resources (Client, Job, Quote, Invoice) | Direct link to resource in Jobber web UI |
previewUrl |
Quotes | Client Hub link for customer approval |
Quick Start
Run the complete example:
uv run examples/visual_confirmation_urls.py
Learn more: Visual Confirmation URLs Guide - Comprehensive patterns, best practices, and use cases.
Pro tip: ALWAYS include jobberWebUri in mutations for instant visual verification!
API Reference
JobberClient
Main client for executing GraphQL queries.
JobberClient.from_doppler(project, config)
Create client loading credentials from Doppler.
Parameters:
project(str): Doppler project name (default: "claude-config")config(str): Doppler config name (default: "dev")
Returns: JobberClient instance
Raises:
ConfigurationError: Doppler secrets not foundAuthenticationError: Token loading fails
Example:
client = JobberClient.from_doppler("claude-config", "dev")
client.execute_query(query, variables=None, operation_name=None)
Execute GraphQL query.
Parameters:
query(str): GraphQL query stringvariables(dict, optional): Query variablesoperation_name(str, optional): Operation name for multi-operation queries
Returns: dict - Response data (response['data'])
Raises:
AuthenticationError: Token invalid or expiredRateLimitError: Rate limit threshold exceeded (< 20% points available)GraphQLError: Query execution failedNetworkError: HTTP request failed
Example:
result = client.execute_query(
query="""
query GetClients($first: Int!) {
clients(first: $first) {
nodes { id firstName }
}
}
""",
variables={'first': 50}
)
client.get_throttle_status()
Get last known rate limit status.
Returns: dict or None
currentlyAvailable: Points available nowmaximumAvailable: Total capacity (typically 10,000)restoreRate: Points restored per second (typically 500)
Example:
status = client.get_throttle_status()
if status:
print(f"{status['currentlyAvailable']} points available")
Error Handling
All methods raise exceptions on failure. Handle appropriately:
from jobber import (
JobberClient,
AuthenticationError,
RateLimitError,
GraphQLError,
NetworkError,
)
try:
client = JobberClient.from_doppler()
result = client.execute_query("{ clients { totalCount } }")
except AuthenticationError as e:
# Resolution: Run jobber_auth.py
print(f"Auth error: {e}")
except RateLimitError as e:
# Resolution: Wait for points to restore
wait_seconds = e.context.get('wait_seconds', 0)
print(f"Rate limited. Wait {wait_seconds:.1f}s")
except GraphQLError as e:
# Resolution: Check query syntax
print(f"Query error: {e.errors}")
except NetworkError as e:
# Resolution: Check connectivity
print(f"Network error: {e}")
Examples
See examples/ directory:
basic_usage.py: Simple queries and paginationerror_handling.py: Comprehensive error handling patterns
Run examples:
uv run --with . examples/basic_usage.py
Architecture
See ADR-0001: Jobber API Client Architecture for design decisions.
Key Principles
- Fail-fast: Raise exceptions immediately, no retry or fallback
- Caller control: AI agent decides error recovery strategy
- Minimal abstraction: Thin wrapper over GraphQL HTTP requests
- Explicit configuration: No default values or silent assumptions
Module Structure
jobber/
├── __init__.py # Public API exports
├── client.py # JobberClient class
├── auth.py # TokenManager (Doppler integration)
├── graphql.py # GraphQLExecutor (HTTP requests)
└── exceptions.py # Exception hierarchy
Rate Limiting
Jobber API limits:
- 10,000 points available
- 500 points/second restore rate
- Query costs vary (9-50 points typical)
This library:
- Raises
RateLimitErrorwhen available points < 20% of maximum - Exposes
throttle_statusin exception context - Caller decides when to wait or abort
Token Management
Tokens expire after 60 minutes. This library:
- Proactively refreshes 5 minutes before expiration (background thread)
- Reactively refreshes on 401 errors (retry once)
- Updates Doppler with new tokens automatically
Token storage in Doppler:
JOBBER_ACCESS_TOKEN=eyJhbGc...
JOBBER_REFRESH_TOKEN=def502...
JOBBER_TOKEN_EXPIRES_AT=1731873600
SLOs
- Availability: Simple code paths, minimal failure modes
- Correctness: Type-safe responses, validation at boundaries
- Observability: Exceptions include full context and stack traces
- Maintainability: < 500 LOC core library, clear contracts
Development
Running Tests
pytest -v
Type Checking
mypy jobber/
Linting
ruff check jobber/
Contributing
This project uses Conventional Commits and semantic-release.
Commit format:
feat: add custom field support
fix: handle token refresh race condition
docs: update error handling examples
License
MIT
Support
- Jobber API Docs: https://developer.getjobber.com/docs/
- API Support: api-support@getjobber.com
- Issues: https://github.com/tainora/jobber-python-client/issues
Changelog
See CHANGELOG.md (auto-generated by semantic-release).
Metadata
Release files for jobber-python-client 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| jobber_python_client-0.2.1.tar.gz | 268.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| jobber_python_client-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 289.8 kB
Release files / jobber_python_client-0.2.1.tar.gz
| Download URL | jobber_python_client-0.2.1.tar.gz |
|---|---|
| Size | 268.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f9af5a344779040480013bc95b76ec52f8848e657673c9559f6c84452b5df162
|
|
BLAKE2b-256 checksum How to use checksums |
f2d9f49c6ba647ef279191cfbafd9f097c365284195e5bc9ca8aaeb4d51413cb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.11 {"installer":{"name":"uv","version":"0.9.11"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / jobber_python_client-0.2.1-py3-none-any.whl
| Download URL | jobber_python_client-0.2.1-py3-none-any.whl |
|---|---|
| Size | 21.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
442ca5fb4d17cbb6945f7fdfcca81787ec53f79957dd1829d2157990b522c41c
|
|
BLAKE2b-256 checksum How to use checksums |
a254e19ad748e3136b1069e5f91b96aa10e6d79b0f5927b83dd462ad767140b0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.11 {"installer":{"name":"uv","version":"0.9.11"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|