Skip to content

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.