Authentication
pydoover handles authentication automatically for both the Data API and Control API clients. This page covers the two auth flows, profile-based configuration, and how token refresh works under the hood.
Profiles are created by running doover login from the Doover CLI. The CLI handles the device authorisation flow and saves your tokens to a named profile.
Auth Flows
pydoover supports two authentication flows, each designed for a different use case:
Doover2 (User Login)
The Doover2 flow is the standard authentication method for user accounts. It uses a refresh token to obtain and renew access tokens from the Doover auth server.
- Used by: Both
DataClientandControlClient - Credentials: refresh token (obtained via
doover login) - Token refresh: automatic, using the refresh token
- Auth server:
https://auth.doover.com(default)
This is the flow used when you authenticate via a profile.
DataService (Client Credentials)
The DataService flow is designed for service accounts and automated systems. It uses a client ID and client secret pair to obtain access tokens.
- Used by:
DataClientonly - Credentials:
client_id+client_secret - Token refresh: automatic, using the client credentials
- Scope: Data API access only (not the Control API)
from pydoover.api import DataClient
# DataService auth is triggered by providing client_id and client_secret
client = DataClient(
client_id="my-service-id",
client_secret="my-service-secret",
)
The ControlClient does not support the DataService flow. Service account access to the Control API requires a Doover2 refresh token.
Profiles
Profiles store authentication credentials in ~/.doover/config so you do not need to pass tokens directly in your code. Profiles are managed by the doover CLI tool and the ConfigManager class.
Profile Structure
Each profile in the config file stores the following fields:
| Field | Description |
|---|---|
TOKEN | Current access token (JWT) |
TOKEN_EXPIRES | Token expiry as a Unix timestamp |
AGENT_ID | Default agent ID for this profile |
BASE_URL | Control API base URL |
BASE_DATA_URL | Data API base URL |
REFRESH_TOKEN | Refresh token for obtaining new access tokens |
REFRESH_TOKEN_ID | Identifier for the refresh token |
AUTH_SERVER_URL | Auth server URL |
AUTH_SERVER_CLIENT_ID | Client ID for the auth server |
Creating a Profile
Profiles are typically created by logging in with the CLI:
doover login
You can also manage profiles programmatically using ConfigManager:
from pydoover.api.auth import ConfigManager, AuthProfile
config = ConfigManager()
# Create a new profile
profile = AuthProfile(
profile="production",
token="eyJhbGciOiJSUzI1NiIs...",
refresh_token="refresh_token_value",
control_base_url="https://api.doover.com",
data_base_url="https://data.doover.com/api",
auth_server_url="https://auth.doover.com",
)
config.create(profile)
config.write()
Using a Profile
Pass the profile name to any client constructor:
from pydoover.api import DataClient, ControlClient # Both clients can use the same profile data_client = DataClient(profile="production") control_client = ControlClient(profile="production")
You can also pass an AuthProfile object directly instead of a name:
from pydoover.api.auth import ConfigManager
config = ConfigManager()
profile = config.get("production")
client = DataClient(profile=profile)
Authentication Methods
Profile-Based
The recommended approach for interactive use and development. Credentials are loaded from ~/.doover/config:
client = DataClient(profile="default")
Explicit Token
Pass a bearer token directly. Useful for short-lived scripts or when tokens are provided externally:
from datetime import datetime, timezone
client = DataClient(
token="eyJhbGciOiJSUzI1NiIs...",
token_expires=datetime(2026, 6, 1, tzinfo=timezone.utc),
)
If you omit token_expires, the client extracts the exp claim from the JWT automatically. The token_expires parameter accepts a datetime, a Unix timestamp as float or int, or None.
Client Credentials
For service accounts on the Data API:
client = DataClient(
client_id="my-service-id",
client_secret="my-service-secret",
)
Pre-Built Auth Object
Share a single auth client across multiple API clients:
from pydoover.api.auth import Doover2AuthClient
auth = Doover2AuthClient.from_profile("default")
data_client = DataClient(auth=auth)
control_client = ControlClient(auth=auth)
When passing an auth object, do not combine it with other auth parameters (profile, token, client_id, etc.). The client raises a ValueError if conflicting auth configuration is detected.
Automatic Token Refresh
Both auth flows handle token refresh transparently. Before each API request, the client calls ensure_token(), which checks whether the current token needs refreshing.
The refresh logic works as follows:
- The JWT's
expclaim is extracted (without cryptographic verification) to determine the expiry time. - If the token expires within the next 30 seconds, a refresh is triggered.
- For Doover2 auth, the refresh token is sent to the auth server to obtain a new access token.
- For DataService auth, the client credentials are used to obtain a new token.
- The new token and its expiry are stored on the auth client.
If no expiry information is available (no exp claim and no token_expires provided), the token is assumed to be valid and no refresh is attempted.
The AuthProfile Dataclass
AuthProfile is a dataclass that represents a stored profile. It is used both by ConfigManager for persistence and by the auth clients for initialization:
from pydoover.api.auth import AuthProfile
profile = AuthProfile(
profile="my-profile",
token="eyJ...",
token_expires=None, # Extracted from JWT automatically
agent_id="12345",
control_base_url="https://api.doover.com",
data_base_url="https://data.doover.com/api",
refresh_token="refresh_token_value",
refresh_token_id="refresh_token_id_value",
auth_server_url="https://auth.doover.com",
auth_server_client_id="client_id_value",
)
The ConfigManager
ConfigManager reads and writes the ~/.doover/config file. It parses profile blocks and provides methods for CRUD operations:
from pydoover.api.auth import ConfigManager
config = ConfigManager()
# List all profiles
for name, profile in config.entries.items():
print(f"{name}: token={'set' if profile.token else 'unset'}")
# Get a specific profile
profile = config.get("default")
# Delete a profile
config.delete("old-profile")
config.write()
The config file format uses INI-style blocks separated by blank lines:
[profile=default] TOKEN=eyJhbGciOiJSUzI1NiIs... TOKEN_EXPIRES=1748880000.0 AGENT_ID=12345 BASE_URL=https://api.doover.com REFRESH_TOKEN=refresh_token_value BASE_DATA_URL=https://data.doover.com/api AUTH_SERVER_URL=https://auth.doover.com
Default URLs
When no URL is specified, the clients use these defaults:
| URL | Default | Used By |
|---|---|---|
| Data API | https://data.doover.com/api | DataClient / AsyncDataClient |
| Control API | https://api.doover.com | ControlClient / AsyncControlClient |
| Auth Server | https://auth.doover.com | Token refresh (Doover2 flow) |
Related Pages
- Data Client -- DataClient constructor and usage
- Agents & Control API -- ControlClient and resource management
- Cloud API Overview -- high-level overview of both APIs