Authentication¶
Pennylane supports three authentication modes. All of them end up as a Bearer token in the Authorization header, which the SDK sets for you.
Company API token¶
For integrations that manage a single company (the most common case).
Created in Settings > Connectivity > Developers by a company admin. Tokens carry:
- a set of scopes (permissions): read only, or read and write, per resource domain;
- an expiration date: plan the renewal, an expired token throws
Pennylane\Sdk\Exception\AuthenticationException.
use Pennylane\Sdk\Pennylane;
$client = new Pennylane(); // PENNYLANE_API_TOKEN env var
$client = new Pennylane(apiToken: 'tok...'); // explicit
Firm API token¶
For accounting firms operating across their client portfolio. Generated from the firm account. Use the dedicated client:
use Pennylane\Sdk\PennylaneFirm;
$firm = new PennylaneFirm(); // PENNYLANE_FIRM_API_TOKEN env var
The Firm and Company tokens are not interchangeable: they authenticate against different base URLs with different scopes.
OAuth 2.0 (partner apps)¶
If you build an app that other Pennylane companies install, use the authorization-code flow. The SDK ships Pennylane\Sdk\OAuth\OAuthApp helpers for the whole dance: authorizationUrl() to redirect the user, exchangeCode() to trade the returned code for a token pair, and refresh() to renew it.
Once you hold an access token, use it like a company token:
$client = new Pennylane(apiToken: $tokens->accessToken);
Refresh token rotation
Pennylane rotates refresh tokens: every call to refresh() invalidates both previous tokens. Never run two refreshes concurrently, and persist the returned pair immediately.
Scopes¶
Each endpoint requires a scope, documented on the Scope: line of every SDK method docblock (for example customer_invoices:all to create invoices, customer_invoices:readonly to list them). When the token lacks the scope, the API answers 403 and the SDK throws Pennylane\Sdk\Exception\PermissionDeniedException:
use Pennylane\Sdk\Exception\PermissionDeniedException;
try {
$client->customerInvoices->create(/* ... */);
} catch (PermissionDeniedException $e) {
echo $e->getMessage(); // the API's own message, naming the missing scope
echo $e->statusCode; // 403
}
Main v2 scopes: customers, products, customer_invoices, quotes, customer_mandates, billing_subscriptions, commercial_documents, suppliers, supplier_invoices, purchase_requests, journals, ledger_accounts, ledger_entries, categories, transactions, bank_accounts, file_attachments (each as :readonly or :all), plus trial_balance:readonly, fiscal_years:readonly, bank_establishments:readonly and exports:fec, exports:agl, exports:gl.
HTTPS enforcement¶
The SDK refuses to send a bearer token over a plain http connection: constructing a client with an insecure baseUrl throws Pennylane\Sdk\Exception\ConfigurationException before any request is made. The only exception is http on localhost, 127.0.0.1 or ::1, for local development.
new Pennylane(baseUrl: 'http://example.com'); // throws ConfigurationException
Good practices¶
- Never commit a token. Use environment variables or a secret manager.
- Prefer read-only tokens for reporting integrations.
- Give each integration its own token, so you can revoke one without breaking the others, and so the rate limit budget (which is per token) is not shared.