OAuth 2.0 apps¶
Building an app that other Pennylane companies install? Pennylane supports the standard authorization-code flow. Ask Pennylane for partner credentials (client_id, client_secret) first.
The flow¶
from pennylane_sdk.oauth import OAuthApp
app = OAuthApp(
client_id="...",
client_secret="...",
redirect_uri="https://yourapp.example.com/oauth/callback",
)
# 1. Send the user to Pennylane's consent screen
url = app.authorization_url(
scopes=["customer_invoices:all", "customers:all"],
state=anti_csrf_token,
)
# 2. Pennylane redirects back with ?code=...; exchange it
tokens = app.exchange_code(code)
# 3. Use the access token like a regular API token
from pennylane_sdk import Pennylane
client = Pennylane(api_token=tokens.access_token)
AsyncOAuthApp offers the same methods as coroutines.
Token lifetime and refresh¶
Access tokens live 24 hours (tokens.expires_in == 86400). Refresh before expiry:
new_tokens = app.refresh(stored_refresh_token)
save(new_tokens.access_token, new_tokens.refresh_token) # persist IMMEDIATELY
Refresh Token Rotation
Pennylane rotates refresh tokens: every refresh invalidates both previous tokens. Two rules follow:
- Never run two refreshes concurrently for the same connection. The SDK serializes refreshes within one process; across processes, use your own lock.
- Persist the new pair before using it. If you crash after refreshing but before saving, the stored refresh token is dead and the user must re-authorize.
When a refresh token is invalid (revoked, rotated away, expired), the token endpoint answers 401 invalid_grant and the SDK raises AuthenticationError: send the user through the authorization flow again.
The 2026 scopes migration¶
In January 2026, Pennylane replaced the legacy ledger scope with granular scopes and auto-added the new scopes to existing OAuth apps. If your app was created before that, re-authenticating your users once ensures their consents match the new scope model.