Skip to content

Authentication

PyPropertyMe uses OAuth 2.0 with the authorization code flow to authenticate with the PropertyMe API.

Overview

The authentication flow:

  1. User initiates authentication via CLI or programmatically
  2. A local callback server starts to receive the OAuth response
  3. Browser opens to PropertyMe's authorization page
  4. User logs in and authorizes the application
  5. PropertyMe redirects back with an authorization code
  6. The code is exchanged for access and refresh tokens
  7. Tokens are stored for future API calls
  8. Access tokens are automatically refreshed when expired

OAuth 2.0 Flow

sequenceDiagram
    participant User
    participant CLI
    participant Browser
    participant PropertyMe

    User->>CLI: pypropertyme auth
    CLI->>CLI: Start local callback server
    CLI->>CLI: Generate state parameter
    CLI->>Browser: Open authorization URL
    Browser->>PropertyMe: User logs in
    PropertyMe->>Browser: Redirect with auth code + state
    Browser->>CLI: Callback with code
    CLI->>CLI: Verify state matches
    CLI->>PropertyMe: Exchange code for tokens
    PropertyMe->>CLI: Access + refresh tokens
    CLI->>CLI: Save to ~/.pypropertyme/tokens.json

CLI Authentication

The simplest way to authenticate:

pypropertyme auth

This command:

  1. Starts a local web server on the configured redirect URI port
  2. Opens your default browser to PropertyMe's login page
  3. Waits for the OAuth callback
  4. Exchanges the authorization code for tokens
  5. Stores tokens in ~/.pypropertyme/tokens.json

Configuration

Required Environment Variables

Set these in a .env file or your shell:

PROPERTYME_CLIENT_ID=your_client_id_here
PROPERTYME_CLIENT_SECRET=your_client_secret_here
PROPERTYME_REDIRECT_URI=http://localhost:65385/home/callback
PROPERTYME_SCOPES=["activity:read", "communication:read", "contact:read", "property:read", "transaction:read", "offline_access"]
Variable Required Description
PROPERTYME_CLIENT_ID Yes OAuth client ID from PropertyMe
PROPERTYME_CLIENT_SECRET Yes OAuth client secret
PROPERTYME_REDIRECT_URI No Callback URL (default: http://localhost:65385/home/callback)
PROPERTYME_SCOPES No JSON array of OAuth scopes

OAuth Scopes

Common scopes for read-only access:

  • activity:read - Read activity logs
  • communication:read - Read communications
  • contact:read - Read contacts
  • property:read - Read properties, tenancies, jobs, etc.
  • transaction:read - Read financial transactions
  • offline_access - Enables refresh tokens for persistent access

Token Storage

Tokens are stored in ~/.pypropertyme/tokens.json:

{
  "access_token": "eyJ...",
  "refresh_token": "eyJ...",
  "token_type": "Bearer",
  "expires_at": 1234567890,
  "expires_in": 3600
}

Security

The token file contains sensitive credentials. Ensure it has appropriate file permissions and is not committed to version control.

Token Refresh

The PropertyMeAuthProvider handles token refresh automatically:

  1. Before each API request, it checks if the access token is expired
  2. If expired, it uses the refresh token to obtain a new access token
  3. The new tokens are saved to the token file
  4. The API request proceeds with the new access token

Programmatic Authentication

Using the Authenticator

from pypropertyme.auth import PropertyMeAuthenticator

# Create authenticator
auth = PropertyMeAuthenticator(
    client_id="your_client_id",
    client_secret="your_client_secret",
    redirect_url="http://localhost:65385/home/callback",
    scopes=["contact:read", "property:read", "offline_access"],
)

# Start the OAuth flow
# This will open the browser and wait for the callback
token = await auth.authenticate()

# Token dict contains access_token, refresh_token, etc.
print(token["access_token"])

Using Tokens with the Client

import json
from pathlib import Path
from pypropertyme.client import Client

# Load tokens from file
token_path = Path.home() / ".pypropertyme" / "tokens.json"
token = json.loads(token_path.read_text())

# Create client with token
client = Client.get_client(token)

# Make API calls
contacts = await client.contacts.all()

Custom Token Persistence

You can provide a callback to save tokens when they're refreshed:

from pypropertyme.client import Client

def save_token(token: dict):
    """Custom token saver - called when tokens are refreshed."""
    # Save to database, secret manager, etc.
    print(f"Token refreshed: {token['access_token'][:20]}...")

# Create client with token saver callback
client = Client.get_client(
    token=initial_token,
    token_saver=save_token,
)

Authentication Classes

PropertyMeAuthenticator

Handles the initial OAuth flow:

  • Constructs the authorization URL
  • Starts a local callback server
  • Exchanges authorization code for tokens
from pypropertyme.auth import PropertyMeAuthenticator

auth = PropertyMeAuthenticator(
    client_id="...",
    client_secret="...",
    redirect_url="...",
    scopes=["..."],
)

# Get the authorization URL
url = auth.construct_auth_url(state="random-state")

# Exchange code for tokens
token = await auth.get_tokens_from_auth_code(code)

PropertyMeAuthProvider

Kiota authentication provider that:

  • Adds Bearer token to API requests
  • Automatically refreshes expired tokens
  • Calls token saver callback when tokens change
from pypropertyme.auth import PropertyMeAuthProvider

provider = PropertyMeAuthProvider(
    token=token,
    client_id="...",
    client_secret="...",
    token_saver=save_callback,
)

Troubleshooting

"No token found"

Error: No token found. Please run 'pypropertyme auth' first.

Run pypropertyme auth to authenticate.

"Token expired"

The client should automatically refresh tokens. If you see this error, your refresh token may have expired. Run pypropertyme auth again.

"Invalid redirect URI"

Ensure the redirect URI in your .env matches exactly what's configured in the PropertyMe developer portal.

Browser doesn't open

If the browser doesn't open automatically, check the terminal for the authorization URL and open it manually.