Skip to content

Errors

Every exception raised by the SDK derives from Pennylane\Sdk\Exception\PennylaneException, itself a RuntimeException. Catch that base type to handle any SDK failure in one place, or catch a specific subclass to react to one situation.

Exception hierarchy

PennylaneException
├── ApiConnectionException
│   └── ApiTimeoutException
├── ApiStatusException
│   ├── BadRequestException          (400)
│   ├── AuthenticationException      (401)
│   ├── PermissionDeniedException    (403)
│   ├── NotFoundException            (404)
│   ├── ConflictException            (409)
│   ├── ValidationException          (422)
│   ├── RateLimitException           (429)
│   └── ServerException              (5xx)
├── ConfigurationException
├── InvalidRequestBodyException
├── InvalidResponseException
└── WebhookSignatureException

PennylaneException

Base class for every exception thrown by the SDK. Carries no properties of its own beyond the standard \RuntimeException message and previous exception.

ApiConnectionException

The request failed at the transport level and no HTTP response was received (DNS failure, connection refused, TLS error, and so on).

ApiTimeoutException

final class ApiTimeoutException extends ApiConnectionException. The request timed out before a response was received.

ApiStatusException

The API answered with an error status code (an unexpected 3xx, or a 4xx/5xx). Error bodies are not consistent across Pennylane endpoints, so every property is parsed defensively and may be null.

Never log the request that produced this exception verbatim: it carries the Authorization header. Log statusCode, errorCode and getMessage() instead. This exception deliberately does not keep a reference to the PSR-7 request or response.

Property Type Description
statusCode int HTTP status code of the response.
errorCode ?string Machine readable code, when the body provides one.
details mixed Extra context from the error body; the shape varies per endpoint.
rawBody string Response body, truncated for safety.
body ?array Decoded JSON error body, or null when the body is not a JSON array or object.

BadRequestException

HTTP 400: the request is malformed or contains invalid parameters.

AuthenticationException

HTTP 401: the API token is missing, invalid or revoked.

PermissionDeniedException

HTTP 403: the token lacks the OAuth scope required by this endpoint. Every resource method documents its required scope in a Scope: line of its docblock.

NotFoundException

HTTP 404: the record does not exist, or the feature is not enabled on this company.

ConflictException

HTTP 409: the request conflicts with the current state of the record.

ValidationException

HTTP 422: the payload was understood but rejected by validation rules. For unbalanced ledger entries, details may carry the debit and credit totals reported by the API.

RateLimitException

HTTP 429: the per token rate limit was exceeded. The SDK throttles proactively and retries automatically, so this surfaces only after the retry budget is exhausted.

Adds one property on top of ApiStatusException:

Property Type Description
retryAfter ?float Seconds to wait, parsed from the retry-after header when it is present and numeric. null otherwise.

ServerException

HTTP 5xx: Pennylane failed to process the request. The API has no idempotency mechanism, so the SDK never retries a POST after a 5xx: the first attempt may have been processed, and a blind retry could create a duplicate invoice or ledger entry.

ConfigurationException

The SDK is not usable as configured. Raised for a missing API token, an insecure base URL (anything other than https, or http outside localhost), or when no PSR-18 HTTP client can be found or built.

InvalidRequestBodyException

A request body or filter value cannot be sent safely. The most common cause is a PHP float in a payload: Pennylane expects monetary amounts and quantities as strings, and floats would lose decimal precision on accounting data.

InvalidResponseException

The API answered with a success status but the body is not usable JSON. Typical causes are captive portals and misconfigured proxies returning an HTML page with a 200 status.

WebhookSignatureException

A webhook payload failed signature verification. Treat the payload as untrusted and discard it.

Example

<?php

declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use Pennylane\Sdk\Exception\NotFoundException;
use Pennylane\Sdk\Exception\RateLimitException;
use Pennylane\Sdk\Exception\ValidationException;
use Pennylane\Sdk\Pennylane;

$client = new Pennylane();

try {
    $invoice = $client->customerInvoices->get(999999999);
} catch (NotFoundException $e) {
    echo "Invoice not found (HTTP {$e->statusCode}): {$e->getMessage()}\n";
} catch (ValidationException $e) {
    echo "Validation failed: {$e->getMessage()}\n";
    var_dump($e->details);
} catch (RateLimitException $e) {
    echo 'Rate limited, retry after ' . ($e->retryAfter ?? 'unknown') . " seconds\n";
}